0ad/source/lib/res/h_mgr.h

349 lines
12 KiB
C
Executable File

// handle manager
//
// Copyright (c) 2003 Jan Wassenberg
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation; either version 2 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// Contact info:
// Jan.Wassenberg@stud.uni-karlsruhe.de
// http://www.stud.uni-karlsruhe.de/~urkt/
#ifndef H_MGR_H__
#define H_MGR_H__
#ifdef __cplusplus
extern "C" {
#endif
#include <stdarg.h> // type init routines get va_list of args
#include "../types.h"
// handle type (for 'type safety' - can't use a texture handle as a sound)
//
// rationale: we could use the destructor passed to h_alloc to identify
// the handle, but it's good to have a list of all types, and we avoid having
// to create empty destructors for handle types that wouldn't need them.
// finally, we save memory - this fits in a few bits, vs. needing a pointer.
// registering extension for each module is bad - some may use many
// (e.g. texture - many formats).
// handle manager shouldn't know about handle types
/*
enum H_Type
{
H_Mem = 1,
H_ZArchive = 2,
H_ZFile = 3,
H_VFile = 4,
H_VRead = 5,
H_Tex = 6,
H_Font = 7,
H_Sound = 8,
NUM_HANDLE_TYPES
};
*/
/*
///xxx advantage of manual vtbl:
no boilerplate init, h_alloc calls ctor directly, make sure it fits in the memory slot
vtbl contains sizeof resource data, and name!
but- has to handle variable params, a bit ugly
*/
// 'manual vtbl' type id
// handles have a type, to prevent using e.g. texture handles as a sound.
//
// alternatives:
// - enum of all handle types (smaller, have to pass all methods to h_alloc)
// - class (difficult to compare type, handle manager needs to know of all users)
//
// checked in h_alloc:
// - user_size must fit in what the handle manager provides
// - name must not be 0
//
// init: user data is initially zeroed
// dtor: user data is zeroed afterwards
// reload: if this resource type is opened by another resource's reload,
// our reload routine MUST check if already opened! This is relevant when
// a file is reloaded: if e.g. a sound object opens a file, the handle
// manager calls the reload routines for the 2 handles in unspecified order.
// ensuring the order would require a tag field that can't overflow -
// not really guaranteed with 32-bit handles. it'd also be more work
// to sort the handles by creation time, or account for several layers of
// dependencies.
struct H_VTbl
{
void(*init)(void* user, va_list);
int(*reload)(void* user, const char* fn);
void(*dtor)(void* user);
size_t user_size;
const char* name;
};
typedef H_VTbl* H_Type;
#define H_TYPE_DEFINE(t)\
/* forward decls */\
static void t##_init(t*, va_list);\
static int t##_reload(t*, const char*);\
static void t##_dtor(t*);\
static H_VTbl V_##t =\
{\
(void(*)(void*, va_list))t##_init,\
(int(*)(void*, const char*))t##_reload,\
(void(*)(void*))t##_dtor,\
sizeof(t), /* control block size */\
#t /* name */\
};\
static H_Type H_##t = &V_##t;
// note: we cast to void* pointers so the functions can be declared to
// take the control block pointers, instead of requiring a cast in each.
// the forward decls ensure the function signatures are correct.
// <type>* <var> = H_USER_DATA(<h_var>, <type>)
#define H_USER_DATA(h, type) (type*)h_user_data(h, H_##type);
#define H_DEREF(h, type, var)\
type* const var = (type*)h_user_data(h, H_##type);\
if(!var)\
return ERR_INVALID_HANDLE;
// 0 = invalid handle value; < 0 is an error code.
// 64 bits, because we want tags to remain unique: tag overflow may
// let handle use errors slip through, or worse, cause spurious errors.
// with 32 bits, we'd need >= 12 for the index, leaving < 512K tags -
// not a lot.
typedef i64 Handle;
// all functions check the passed tag (part of the handle) and type against
// the internal values. if they differ, an error is returned.
// resource scope
// used together with flags (e.g. in mem), so no separate type
enum
{
RES_TEMP = 1,
RES_LEVEL = 2,
RES_STATIC = 3
};
#define RES_SCOPE_MASK 3
// h_alloc flags
enum
{
// the resource isn't backed by a file. the fn parameter is treated as the search key (uintptr_t)
// currently only used by mem manager
RES_KEY = 4
};
// allocate a new handle.
// if key is 0, or a (key, type) handle doesn't exist,
// the first free entry is used.
// otherwise, a handle to the existing object is returned,
// and HDATA.size != 0.
//// user_size is checked to make sure the user data fits in the handle data space.
// dtor is associated with type and called when the object is freed.
// handle data is initialized to 0; optionally, a pointer to it is returned.
extern Handle h_alloc(H_Type type, const char* fn, uint flags = 0, ...);
extern int h_free(Handle& h, H_Type type);
/*
// find and return a handle by key (typically filename hash)
// currently O(n).
extern Handle h_find(H_Type type, uintptr_t key);
*/
// return a pointer to handle data
extern void* h_user_data(Handle h, H_Type type);
extern const char* h_filename(Handle h);
// some resource types are heavyweight and reused between files (e.g. VRead).
// this reassigns dst's (of type <type>) associated file to that of src.
int h_reassign(const Handle dst, const H_Type dst_type, const Handle src);
extern int res_cur_scope;
#ifdef __cplusplus
}
#endif
#endif // #ifndef H_MGR_H__
// introduction
// ------------
//
// a resource is an instance of a specific type of game data (e.g. texture),
// described by a control block (example fields: format, pointer to tex data).
//
// this module allocates storage for the control blocks, which are accessed
// via handle. it also provides support for transparently reloading resources
// from disk (allows in-game editing of data), and caches resource data.
// finally, it frees all resources at exit, preventing leaks.
//
//
// handles
// -------
//
// handles are an indirection layer between client code and resources
// (represented by their control blocks, which contains/points to its data).
// they allow an important check not possible with a direct pointer:
// guaranteeing the handle references a given resource /instance/.
//
// problem: code C1 allocates a resource, and receives a pointer p to its
// control block. C1 passes p on to C2, and later frees it.
// now other code allocates a resource, and happens to reuse the free slot
// pointed to by p (also possible if simply allocating from the heap).
// when C2 accesses p, the pointer is valid, but we cannot tell that
// it is referring to a resource that had already been freed. big trouble.
//
// solution: each allocation receives a unique tag (a global counter that
// is large enough to never overflow). Handles include this tag, as well
// as a reference (array index) to the control block, which isn't directly
// accessible. when dereferencing the handle, we check if the handle's tag
// matches the copy stored in the control block. this protects against stale
// handle reuse, double-free, and accidentally referencing other resources.
//
// type: each handle has an associated type. these must be checked to prevent
// using textures as sounds, for example. with the manual vtbl scheme,
// this type is actually a pointer to the resource object's vtbl, and is
// set up via H_TYPE_DEFINE. this means that types are private to the module
// that declared the handle; knowledge of the type ensures the caller
// actually declared, and owns the resource.
// guide to defining and using resources
// -------------------------------------
//
// 1) choose a name for the resource, used to represent all resources
// of this type. we will call ours Res1; all occurences of it below
// must be replaced with the actual name (exact spelling).
// why? the vtbl builder defines its functions as e.g. Res1_reload;
// your actual definition must match.
//
// 2) declare its control block:
// struct Res1
// {
// void* data1; // data loaded from file
// int flags; // set when resource is created
// };
//
// 3) build its vtbl:
// H_TYPE_DEFINE(Res1)
//
// this defines the symbol H_Res1, which is used whenever the handle
// manager needs its type. it is only accessible to this module
// (file scope). note that it is actually a pointer to the vtbl.
// this must come before uses of H_Res1, and after the CB definition;
// there are no restrictions WRT functions, because the macro
// forward-declares what it needs.
//
// 4) implement all 'virtual' functions from the resource interface.
// note that inheritance isn't really possible with this approach -
// all functions must be defined, even if not needed.
//
// --
//
// init:
// one-time init of the control block. called from h_alloc.
// precondition: control block is initialized to 0.
//
// static void Type_init(Res1* r, va_list args)
// {
// r->flags = va_arg(args, int);
// }
//
// if the caller of h_alloc passed additional args, they are available
// in args. if init references more args than were passed, big trouble.
// however, this is a bug in your code, and cannot be triggered
// maliciously. only your code knows the resource type, and it is the
// only call site of h_alloc.
// there is no provision for indicating failure. if one-time init fails
// (rare, but one example might be failure to allocate memory that is
// for the lifetime of the resource, instead of in reload), it will
// have to set the control block state such that reload will fail.
//
// --
//
// reload:
// does all initialization of the resource that requires its source file.
// called after init; also after dtor every time the file is reloaded.
//
// static int Type_reload(Res1* r, const char* filename);
// {
// // somehow load stuff from filename, and store it in r->data1.
// return 0;
// }
//
// reload must abort if the control block data indicates the resource
// has already been loaded! example: if texture's reload is called first,
// it loads itself from file (triggering file.reload); afterwards,
// file.reload will be called again. we can't avoid this, because the
// handle manager doesn't know anything about dependencies
// (here, texture -> file).
// return value: 0 if successful (includes 'already loaded'),
// negative error code otherwise. if this fails, the resource is freed
// (=> dtor is called!).
//
// --
//
// dtor:
// frees all data allocated by init and reload. called after h_free,
// or at exit. control block is zeroed afterwards.
//
// static void Type_dtor (Res1* r);
// {
// // free memory r->data1
// }
//
// again no provision for reporting errors - there's no one to act on it
// if called at exit. you can assert or log the error, though.
//
// 5) provide your layer on top of the handle manager:
// Handle res1_load(const char* filename, int my_flags)
// {
// return h_alloc(H_Res1, filename, 0, /* additional param passed to init -> */ my_flags);
// }
//
// int res1_free(Handle& h)
// {
// return h_free(h, H_Res1);
// }
//
// the h parameter is zeroed by h_free.
//
// (this layer allows a res_load interface on top of all the loaders,
// and is necessary because your module is the only one that knows H_Res1).
//
// 6) done. the resource will be freed at exit (if not done already).