| 1 | |
| 2 | introduction |
| 3 | ------------ |
| 4 | |
| 5 | a resource is an instance of a specific type of game data (e.g. texture), |
| 6 | described by a control block (example fields: format, pointer to tex data). |
| 7 | |
| 8 | this module allocates storage for the control blocks, which are accessed |
| 9 | via handle. it also provides support for transparently reloading resources |
| 10 | from disk (allows in-game editing of data), and caches resource data. |
| 11 | finally, it frees all resources at exit, preventing leaks. |
| 12 | |
| 13 | |
| 14 | handles |
| 15 | ------- |
| 16 | |
| 17 | handles are an indirection layer between client code and resources |
| 18 | (represented by their control blocks, which contains/points to its data). |
| 19 | they allow an important check not possible with a direct pointer: |
| 20 | guaranteeing the handle references a given resource /instance/. |
| 21 | |
| 22 | problem: code C1 allocates a resource, and receives a pointer p to its |
| 23 | control block. C1 passes p on to C2, and later frees it. |
| 24 | now other code allocates a resource, and happens to reuse the free slot |
| 25 | pointed to by p (also possible if simply allocating from the heap). |
| 26 | when C2 accesses p, the pointer is valid, but we cannot tell that |
| 27 | it is referring to a resource that had already been freed. big trouble. |
| 28 | |
| 29 | solution: each allocation receives a unique tag (a global counter that |
| 30 | is large enough to never overflow). Handles include this tag, as well |
| 31 | as a reference (array index) to the control block, which isn't directly |
| 32 | accessible. when dereferencing the handle, we check if the handle's tag |
| 33 | matches the copy stored in the control block. this protects against stale |
| 34 | handle reuse, double-free, and accidentally referencing other resources. |
| 35 | |
| 36 | type: each handle has an associated type. these must be checked to prevent |
| 37 | using textures as sounds, for example. with the manual vtbl scheme, |
| 38 | this type is actually a pointer to the resource object's vtbl, and is |
| 39 | set up via H_TYPE_DEFINE. this means that types are private to the module |
| 40 | that declared the handle; knowledge of the type ensures the caller |
| 41 | actually declared, and owns the resource. |
| 42 | |
| 43 | |
| 44 | guide to defining and using resources |
| 45 | ------------------------------------- |
| 46 | |
| 47 | 1) choose a name for the resource, used to represent all resources |
| 48 | of this type. we will call ours Res1; all occurences of it below |
| 49 | must be replaced with the actual name (exact spelling). |
| 50 | why? the vtbl builder defines its functions as e.g. Res1_reload; |
| 51 | your actual definition must match. |
| 52 | |
| 53 | 2) declare its control block: |
| 54 | {{{ |
| 55 | struct Res1 |
| 56 | { |
| 57 | void* data1; // data loaded from file |
| 58 | uint flags; // set when resource is created |
| 59 | }; |
| 60 | }}} |
| 61 | |
| 62 | 3) build its vtbl: |
| 63 | {{{ |
| 64 | H_TYPE_DEFINE(Res1); |
| 65 | }}} |
| 66 | |
| 67 | this defines the symbol H_Res1, which is used whenever the handle |
| 68 | manager needs its type. it is only accessible to this module |
| 69 | (file scope). note that it is actually a pointer to the vtbl. |
| 70 | this must come before uses of H_Res1, and after the CB definition; |
| 71 | there are no restrictions WRT functions, because the macro |
| 72 | forward-declares what it needs. |
| 73 | |
| 74 | 4) implement all 'virtual' functions from the resource interface. |
| 75 | note that inheritance isn't really possible with this approach - |
| 76 | all functions must be defined, even if not needed. |
| 77 | |
| 78 | -- |
| 79 | |
| 80 | init: |
| 81 | one-time init of the control block. called from h_alloc. |
| 82 | precondition: control block is initialized to 0. |
| 83 | |
| 84 | {{{ |
| 85 | static void Type_init(Res1* r, va_list args) |
| 86 | { |
| 87 | r->flags = va_arg(args, int); |
| 88 | } |
| 89 | }}} |
| 90 | |
| 91 | if the caller of h_alloc passed additional args, they are available |
| 92 | in args. if init references more args than were passed, big trouble. |
| 93 | however, this is a bug in your code, and cannot be triggered |
| 94 | maliciously. only your code knows the resource type, and it is the |
| 95 | only call site of h_alloc. |
| 96 | there is no provision for indicating failure. if one-time init fails |
| 97 | (rare, but one example might be failure to allocate memory that is |
| 98 | for the lifetime of the resource, instead of in reload), it will |
| 99 | have to set the control block state such that reload will fail. |
| 100 | |
| 101 | -- |
| 102 | |
| 103 | reload: |
| 104 | does all initialization of the resource that requires its source file. |
| 105 | called after init; also after dtor every time the file is reloaded. |
| 106 | |
| 107 | {{{ |
| 108 | static int Type_reload(Res1* r, const char* filename, Handle); |
| 109 | { |
| 110 | // somehow load stuff from filename, and store it in r->data1. |
| 111 | return 0; |
| 112 | } |
| 113 | }}} |
| 114 | |
| 115 | reload must abort if the control block data indicates the resource |
| 116 | has already been loaded! example: if texture's reload is called first, |
| 117 | it loads itself from file (triggering file.reload); afterwards, |
| 118 | file.reload will be called again. we can't avoid this, because the |
| 119 | handle manager doesn't know anything about dependencies |
| 120 | (here, texture -> file). |
| 121 | return value: 0 if successful (includes 'already loaded'), |
| 122 | negative error code otherwise. if this fails, the resource is freed |
| 123 | (=> dtor is called!). |
| 124 | |
| 125 | note that any subsequent changes to the resource state must be |
| 126 | stored in the control block and 'replayed' when reloading. |
| 127 | example: when uploading a texture, store the upload parameters |
| 128 | (filter, internal format); when reloading, upload again accordingly. |
| 129 | |
| 130 | -- |
| 131 | |
| 132 | dtor: |
| 133 | frees all data allocated by init and reload. called after h_free, |
| 134 | or at exit. control block will be zeroed afterwards. |
| 135 | |
| 136 | {{{ |
| 137 | static void Type_dtor (Res1* r); |
| 138 | { |
| 139 | // free memory r->data1 |
| 140 | } |
| 141 | }}} |
| 142 | |
| 143 | again no provision for reporting errors - there's no one to act on it |
| 144 | if called at exit. you can assert or log the error, though. |
| 145 | |
| 146 | 5) provide your layer on top of the handle manager: |
| 147 | {{{ |
| 148 | Handle res1_load(const char* filename, int my_flags) |
| 149 | { |
| 150 | return h_alloc(H_Res1, filename, 0, my_flags); // my_flags is passed to init |
| 151 | } |
| 152 | }}} |
| 153 | |
| 154 | {{{ |
| 155 | int res1_free(Handle& h) |
| 156 | { |
| 157 | return h_free(h, H_Res1); |
| 158 | // zeroes h afterwards |
| 159 | } |
| 160 | }}} |
| 161 | |
| 162 | (this layer allows a res_load interface on top of all the loaders, |
| 163 | and is necessary because your module is the only one that knows H_Res1). |
| 164 | |
| 165 | 6) done. the resource will be freed at exit (if not done already). |
| 166 | |
| 167 | here's how to access the control block, given a handle: |
| 168 | a) |
| 169 | {{{ |
| 170 | Handle h; |
| 171 | H_DEREF(h, Res1, r); |
| 172 | }}} |
| 173 | |
| 174 | creates a variable r of type Res1*, which points to the control block |
| 175 | of the resource referenced by h. returns "invalid handle" |
| 176 | (a negative error code) on failure. |
| 177 | b) |
| 178 | {{{ |
| 179 | Handle h; |
| 180 | Res1* r = h_user_data(h, H_Res1); |
| 181 | if(!r) |
| 182 | ; // bail |
| 183 | }}} |
| 184 | |
| 185 | useful if H_DEREF's error return (of type signed integer) isn't |
| 186 | acceptable. otherwise, prefer a) - this is pretty clunky, and |
| 187 | we could switch H_DEREF to throwing an exception on error. |