I made a simple scene manager in C99. I tried to make it readable, reusable and reliable. I would be grateful if you would help me to improve it. I want to use it for my upcoming OpenGL game. I think that code is sufficiently self-documenting for you to skip the comments.
The headers
scene_manager.h
// Defines client interface of Scene Manager
#ifndef SCENE_MANAGER_H
#define SCENE_MANAGER_H
#include "scene.h"
// Initialization and termination functions will not fail or yield undefined
// behaviour if called multiple times or in the wrong order. Other functions
// are safe to be called at all times, too. Scenes are the weakest component
// Initializes scene manager. Warning: scenes are not deep copied!
bool initialize_scene_manager(int n_scenes, Scene *scenes, int initial_id);
// Executes the top scene on the scene stack
bool execute_top_scene(void);
// Terminates the scene manager.
void terminate_scene_manager(void);
// Returns whether the scene stack is empty
bool scene_stack_is_empty(void);
// Returns whether the scene stack is not empty
bool scene_stack_is_not_empty(void);
#endif // SCENE_MANAGER_H
scene.h
// Defines interface used by scene manager (SM) to communicate with its scenes
#ifndef SCENE_H
#define SCENE_H
#include <stdbool.h>
#define POP_REQUEST (-1)
// Sends a critical error signal which results in immediate SM termination and
// stderr output if error is not equal to 0 OR description is not equal to NULL
void send_critical_error_signal(int error, const char *description);
// Invalid pointers cause undefined behavior - most likely a segmentation fault
typedef struct Scene
{
// It initializes the scene
// On success, returns a pointer to its private data
// If the scene has no private data, it can return a dummy string literal
// On failure, returns a NULL pointer
// If the INITIAL scene fails to initialize, so does the scene manager
// If a NON-INITIAL scene fails to initialize, the scene manager attempts
// to proceed as if no PUSH request was received (as if nothing happened)
// You can send a critical error signal to override default behavior
// It must not be NULL; else it will result in SM initialization failure
void *(*initialize)(void);
// It reinitializes the scene after it becomes the top one again
// On success, returns true
// On failure, returns false
// Failure will result in the scene being popped from the stack
// It can be NULL. It being NULL will cause it to be ignored
bool (*reinitialize)(void *data);
// It executes the scene
// On success, returns a POP or a valid PUSH request
// On failure, returns an invalid PUSH request
// A valid PUSH request is an a integer between 0 and number of scenes
// Success makes run_top_scene (SM client interface) return true
// Failure makes run_top_scene (SM client interface) return false
// It must not be NULL; else it will result in SM initialization failure
int (*execute)(void *data);
// It terminates the scene.
// It must free all the dynamically allocated data to avoid memory leaks
// It should not fail. If it does, you can send a critical error signal
// If it does but you do not consider the memory leak or other implications
// to be very serious, you can ignore it, though I would not recommend it
// It must not be NULL; else it will result in SM initialization failure
void (*terminate)(void *data);
} Scene;
#endif // SCENE_H
misc_util.h
// Miscellaneous utilities
#ifndef MISC_UTIL_H
#define MISC_UTIL_H
#include <stdbool.h>
#include <stddef.h> // to get size_t
// Function used to allocate memory. Returns true on success, false on failure
bool safer_realloc(void **p, size_t increased_size);
// Function used to deallocate memory. Always succeeds!
void safe_dealloc(void **p, size_t decreased_size);
#endif // MISC_UTIL_H
The source files
scene_manager.c
#include "scene_manager.h"
#include "misc_util.h"
#include <assert.h>
#include <stdio.h>
// Type definitions used in the SM
typedef struct SceneInstance
{
int id;
void *data;
} SceneInstance;
// Data structures of the SM
struct SceneManagerData
{
bool initialized;
int n_scenes;
Scene *scenes;
} sm_data = {false, 0, NULL};
struct SceneManagerStack
{
int height;
SceneInstance *stack;
} sm_stack = {0, NULL};
// Prototypes of subroutine functions used in the client interface of the SM
static bool push_scene(int id); // Tries to push a new scene onto the stack
static void pop_scene(void); // Pops the topmost scene of the stack
void send_critical_error_signal(int error, const char *description)
{
if (error != 0 || description != NULL)
{
fprintf(stderr,
"Critical scene manager error #%d: '%s'\n",
error, description);
}
terminate_scene_manager();
}
bool initialize_scene_manager(int n_scenes, Scene *scenes, int initial_id)
{
if (sm_data.initialized == true)
{
fputs("Warning: attempt to reinitialize scene manager\n", stderr);
return true;
}
else if (n_scenes <= 0 || scenes == NULL ||
initial_id < 0 || initial_id >= n_scenes)
{
fprintf(stderr,
"Invalid scene manager initialization data: %d, %p, %d\n",
n_scenes, (void *) scenes, initial_id);
return false;
}
else
{
for (int i = 0; i < n_scenes; ++i)
{
if ( scenes[i].initialize == NULL
|| scenes[i].execute == NULL
|| scenes[i].terminate == NULL)
{
fprintf(stderr, "Scene (i = %d) has a NULL pointer\n", i);
return false;
}
}
sm_data = (struct SceneManagerData) {true, n_scenes, scenes};
if (push_scene(initial_id) == false)
{
sm_data = (struct SceneManagerData) {false, 0, NULL};
fputs("Failed to initialize initial scene\n", stderr);
return false;
}
else
{
return true;
}
}
}
bool execute_top_scene(void)
{
if (sm_data.initialized == false)
{
fputs("Warning: attempt to run a scene in "
"uninitialized scene manager\n", stderr);
return false;
}
else if (scene_stack_is_empty())
{
fputs("Warning: attempt to run a scene in "
"empty scene manager\n", stderr);
return false;
}
else
{
assert(sm_data.n_scenes > 0);
assert(sm_data.scenes != NULL);
assert(sm_stack.stack != NULL);
int index = sm_stack.height - 1;
int id = sm_stack.stack[index].id;
void *data = sm_stack.stack[index].data;
assert(id >= 0);
assert(id < sm_data.n_scenes);
assert(data != NULL);
assert(sm_data.scenes[id].execute != NULL);
int return_value = sm_data.scenes[id].execute(data);
// CASE 1: POP REQUEST
if (return_value == POP_REQUEST)
{
pop_scene();
// Reinitialize the new top scene if there is one
if (scene_stack_is_not_empty())
{
bool reinit = true;
do // While there IS a top scene with a reinitialize
{ // function and it fails, pop another scene
int new_index = sm_stack.height - 1;
int new_id = sm_stack.stack[new_index].id;
void *new_data = sm_stack.stack[new_index].data;
assert(new_id >= 0);
assert(new_id < sm_data.n_scenes);
assert(new_data != NULL);
reinit = true;
if (sm_data.scenes[new_id].reinitialize)
{
reinit = sm_data.scenes[new_id].reinitialize(new_data);
if (reinit == false)
{
fprintf(stderr,
"Warning: scene (id=%d) failed to reinit\n",
new_id);
pop_scene();
}
}
} while (scene_stack_is_not_empty() && reinit == false);
}
return true;
}
// CASE 2: PUSH REQUEST
else if (return_value >= 0 && return_value < sm_data.n_scenes)
{
return push_scene(return_value); // stderr output inside
}
// CASE 3: INVALID VALUE
else
{
fprintf(stderr,
"Warning: scene (id = %d) returned an invalid value\n",
id);
return false;
}
}
}
static bool push_scene(int id)
{
assert(sm_data.initialized == true);
assert(sm_data.n_scenes > 0);
assert(sm_data.scenes != NULL);
assert(sm_stack.height >= 0);
assert(id >= 0);
assert(id < sm_data.n_scenes);
assert(sm_data.scenes[id].initialize != NULL);
size_t old_size = sm_stack.height * sizeof(*sm_stack.stack);
size_t new_size = old_size + sizeof(*sm_stack.stack);
if(!safer_realloc((void **) &sm_stack.stack, new_size))
{
fputs("Failed to expand scene stack\n", stderr);
return false;
}
else
{
void *data = sm_data.scenes[id].initialize();
if(data == NULL)
{
fprintf(stderr, "Warning: scene (%d) failed to initialize\n", id);
safe_dealloc((void **) &sm_stack.stack, old_size);
return false;
}
else
{
int index = sm_stack.height;
sm_stack.stack[index] = (SceneInstance) {id, data};
sm_stack.height += 1;
return true;
}
}
}
static void pop_scene(void)
{
assert(sm_data.initialized == true);
assert(sm_data.n_scenes > 0);
assert(sm_data.scenes != NULL);
assert(sm_stack.height > 0);
assert(sm_stack.stack != NULL);
int index = sm_stack.height - 1;
int id = sm_stack.stack[index].id;
void *data = sm_stack.stack[index].data;
assert(id >= 0);
assert(id < sm_data.n_scenes);
assert(data != NULL);
assert(sm_data.scenes[id].terminate != NULL);
sm_stack.height -= 1;
size_t new_size = sm_stack.height * sizeof(*sm_stack.stack);
sm_data.scenes[id].terminate(data);
safe_dealloc((void **) &sm_stack.stack, new_size);
}
void terminate_scene_manager(void)
{
if (sm_data.initialized == false)
{
fputs("Warning: attempt to terminate "
"uninitinialized scene manager\n", stderr);
}
else
{
while (scene_stack_is_not_empty())
{
pop_scene();
}
sm_data = (struct SceneManagerData) {false, 0, NULL};
}
}
bool scene_stack_is_empty(void)
{
if (sm_data.initialized == false)
{
fputs("Warning: inquiry of stack height of an "
"uninitialized scene manager\n", stderr);
assert(sm_stack.height == 0);
}
assert(sm_stack.height >= 0);
return sm_stack.height == 0;
}
bool scene_stack_is_not_empty(void)
{
return !scene_stack_is_empty();
}
misc_util.c
#include "misc_util.h"
#include <assert.h>
#include <stdlib.h>
bool safer_realloc(void **p, size_t increased_size)
{
assert(p != NULL);
void *tmp = realloc(*p, increased_size);
if (tmp == NULL)
{
return false;
}
else
{
*p = tmp;
return true;
}
}
void safe_dealloc(void **p, size_t decreased_size)
{
assert(p != NULL);
if (decreased_size == 0)
{
free(*p);
*p = NULL;
}
else
{
void *tmp = realloc(*p, decreased_size);
*p = tmp ? tmp : *p;
}
}
A quick and dirty demonstration
main.c
// A quick and dirty, short demonstration of Scene Manager capabilities
#include "scene_manager.h"
#include <stdlib.h>
#include <stdio.h>
enum Scenes
{
SCENE_ONE,
SCENE_TWO,
NUM_SCENES
};
// Function prototypes
void *initialize_scene_one(void);
bool reinitialize_scene_one(void *data);
int execute_scene_one(void *data);
void terminate_scene_one(void *data);
void *initialize_scene_two(void);
int execute_scene_two(void *data);
void terminate_scene_two(void *data);
// Scenes
Scene scene_one = {initialize_scene_one,
reinitialize_scene_one,
execute_scene_one,
terminate_scene_one};
Scene scene_two = {initialize_scene_two,
NULL,
execute_scene_two,
terminate_scene_two};
// Function implementations
void *initialize_scene_one(void)
{
puts("Initializing scene ONE");
return malloc(100);
}
bool reinitialize_scene_one(void *data)
{
puts("Reinitializing scene ONE");
return true;
}
int execute_scene_one(void *data)
{
puts("Executing scene ONE");
static bool first_run = true;
if (first_run == true)
{
first_run = false;
return SCENE_TWO;
}
else
{
return POP_REQUEST;
}
}
void terminate_scene_one(void *data)
{
puts("Terminating scene ONE");
free(data);
}
void *initialize_scene_two(void)
{
puts("Initializing scene TWO");
return "Executing scene TWO";
}
int execute_scene_two(void *data)
{
puts(data);
return POP_REQUEST;
}
void terminate_scene_two(void *data)
{
puts("Terminating scene TWO");
}
// The main function
int main(void)
{
Scene scenes[NUM_SCENES];
scenes[0] = scene_one;
scenes[1] = scene_two;
if (initialize_scene_manager(NUM_SCENES, scenes, SCENE_ONE) == true)
{
while (scene_stack_is_not_empty())
{
if (execute_top_scene() == false)
{
break;
}
}
terminate_scene_manager();
}
return EXIT_SUCCESS;
}
stdout
output (everything works as expected)
Initializing scene ONE
Executing scene ONE
Initializing scene TWO
Executing scene TWO
Terminating scene TWO
Reinitializing scene ONE
Executing scene ONE
Terminating scene ONE
GitHub repository if you would like to test it
Note: I don't know how to make a proper makefile because I'm used to IDEs. I'm fine with them for now.
1 Answer 1
Small review
safe_dealloc()
and safer_realloc()
could be rolled into one. safer_realloc(0)
would benefit clear size==0 functionality.
Pedantic: size_t new_size = old_size + sizeof(*sm_stack.stack);
lacks overflow protection.
[Edit] Sample merge
Since the calling code essentially needs to keep track of the present size, pass then both in by address and let the pointer and size variables get updated together in this function.
bool safer_realloc(void **p, size_t *current_size, size_t new_size) {
assert(p != NULL);
assert(Current_size != NULL);
if (new_size == 0) {
free(*p);
*p = NULL;
*current_size = 0;
return true; // OK;
}
void *tmp = realloc(*p, new_size);
if (tmp == NULL) {
// no change to *p or *current_size
return false; // !OK
}
*p = tmp;
*current_size = new_size;
return true; // OK;
}
When calling code is shrinking, no strong need to check the return value.
-
\$\begingroup\$ How would you merge
safe_dealloc
andsafer_realloc?
I understandsafer_realloc(&ptr, 0)
functionality, but how could one know whether memory is allocated or deallocated insafer_realloc(&ptr, 42)
scenario? Do you suggest passing a boolean flag? To clarify, in the proposed scenario, both functions would behave distinctly differently.safe_dealloc
would deliver a bulletproof pointer whilesafer_realloc
might fail if the machine is short of 42 bytes. Now that I think of it, maybe I should renamesafer_realloc
tosafer_alloc
? \$\endgroup\$Sand Freight– Sand Freight2016年02月02日 17:44:59 +00:00Commented Feb 2, 2016 at 17:44 -
\$\begingroup\$ "When calling code is shrinking, no strong need to check the return value." What about this? Is this phenomena too rare to be bothered about? \$\endgroup\$Sand Freight– Sand Freight2016年02月02日 18:00:25 +00:00Commented Feb 2, 2016 at 18:00
-
\$\begingroup\$ @Sand Freight No strong need for the calling code to check the result of
safer_realloc()
when it is shrinking.safer_realloc()
still needs to check the result ofrealloc()
on shrink/grow/same-size. \$\endgroup\$chux– chux2016年02月02日 18:02:38 +00:00Commented Feb 2, 2016 at 18:02 -
\$\begingroup\$ @Sand Freight Since the code uses a pointer and size as a pair, consider making that a structure. \$\endgroup\$chux– chux2016年02月02日 18:05:50 +00:00Commented Feb 2, 2016 at 18:05
-
1\$\begingroup\$ Okay, I will let this stay for another 24 hours. \$\endgroup\$Sand Freight– Sand Freight2016年02月02日 18:09:10 +00:00Commented Feb 2, 2016 at 18:09