/* ** Example: memory management ** ** This example shows how to use Metadesk in a program where memory management ** is important. For example if Metadesk is used as the basis for a config file ** system in an application, you may need to do things like reloading a config, ** or managing multiple configs with different arbitrary lifetimes. ** ** In the case of the simple metaprogram we manage memory by just having one ** global arena, and we never free anything. In this case we'll start using ** multiple arenas to create distinct lifetime "buckets" or "groups". ** ** Comments in this example explains a little about how Metadesk arenas work, ** and tips for using them effectively. ** */ //~ includes and globals ////////////////////////////////////////////////////// // @notes In this example we're just using the default implementations of // the low level memory allocators. There's nothing wrong with doing it this // way but in a codebase where something like a config file matters there // may very well already be some custom allocators. Check out the overrides // example for details on how to plug in custom allocators. // // Everything shown here about the Metadesk arena remains true either way, // because arenas are implemented on top of the low level memory allocator. // Basically you can think of the arena as performing allocation batching with // whatever low level memory allocator is available. #include "md.h" #include "md.c" //~ pretend config file /////////////////////////////////////////////////////// // @notes In this example we'll pretend we have a config file system, but we // only show the part up to finishing the Metadesk parse. Each ConfigFile will // carry a Metadesk arena, which handles the memory, and every version of the // file data at each stage of processing. // // In a real system there would likely be at least one more stage of // processing where a more processed version of the config that is made from // analyzing the Metadesk tree. // // Here we can release a ConfigFile by simply releasing the arena because we // have followed the simple rule that everything in the ConfigFile is // *allocated on the arena*. If we were doing the analysis phase, we could // keep this working by just writing the analyzer to allocate on the arena // too. // // An alternative approach here that could save memory is to use the arena // as an allocator for temporary intermediates. In this approach during the // analysis stage the final data structure would be allocated outside the // arena used for parsing, and everything that the final structure needs would // be copied out. Then the parse could be thrown away. This approach saves // memory at the cost of making problems harder to trouble shoot, and // sometimes more time in analysis to copy things out of the temporary arena. typedef struct ConfigFile{ MD_Arena *arena; MD_String8 file_name; MD_String8 contents; Node *root; MD_MessageList errors; } ConfigFile; ConfigFile* new_config_file_from_file_name(char *file_name_cstr) { MD_Arena *arena = MD_ArenaAlloc(); // @notes MD_PushArray and MD_PushArrayZero are the fundamental allocation // operations to do with a Metadesk arena. Metadesk APIs that take an // MD_Arena* parameter are APIs that need to do allocation. When we pass // an arena in one of these APIs the returned data is allocated using that // arena and we say that the data is "allocated on the arena". ConfigFile *result = MD_PushArrayZero(arena, ConfigFile, 1); // @notes We explicitly copy the file name onto the arena so that we can // be totally sure that it has the same lifetime as everything else in // the ConfigFile. MD_String8 file_name = MD_S8Copy(arena, MD_S8CString(file_name_cstr)); // @notes Here we break down MD_ParseWholeFile into it's two stages // explicitly so that we can save the contents and the parse in the // ConfigFile. MD_String8 contents = MD_LoadEntireFile(arena, file_name); ParseResult parse = MD_ParseWholeString(arena, file_name, contents); // @notes This part can be a little bit subtle. First we allocated the // arena with MD_ArenaAlloc. Then we used the arena to allocate a // ConfigFile. Now we are storing a pointer to the arena in the config. // The subtle part is that if you're used to thinking in terms of // 'ownership' it seems like the config owns the arena, but if we release // the arena, we also release the config. // // A different way to think about it is that the arena is a handle that // manages allocation lifetimes. The ConfigFile and all of the data it // holds share the same lifetime, so they are all allocated on the same // "lifetime handle" (i.e. the same arena). The ConfigFile is the root of // all that data so it also holds the handle for releasing later. result->arena = arena; result->file_name = file_name; result->contents = contents; result->root = parse.node; result->errors = parse.errors; return(result); } void release_config_file(ConfigFile *file) { MD_ArenaRelease(file->arena); } //~ just to simulate new config files coming from somewhere /////////////////// int in_files_count = 0; char** in_file_names = 0; int in_file_iter = 0; ConfigFile* new_config_file(void) { ConfigFile *result = new_config_file_from_file_name(in_file_names[in_file_iter]); in_file_iter = (in_file_iter + 1)%in_files_count; return(result); } //~ main ////////////////////////////////////////////////////////////////////// int main(int argc, char **argv) { // make sure we have something to parse if (argc <= 1) { fprintf(stderr, "pass at least one input file"); exit(1); } // setup the the source of files in_files_count = argc - 1; in_file_names = argv + 1; // @notes The idea here is to simulate a situation where an allocate and // never free strategy would lead to growing memory usage over time // (i.e. a memory leak). // pretend there are unpredictable lifetimes tied to real-time events { // first we get three config files ConfigFile *files[3]; files[0] = new_config_file(); files[1] = new_config_file(); files[2] = new_config_file(); // then we chaotically replace the slots for a while for (MD_u32 i = 10000; i < 20000; i += 1) { MD_u32 x = (i >> (i&3)) ^ (i << (16 + (i&3))); MD_u32 slot_index = x%3; release_config_file(files[slot_index]); files[slot_index] = new_config_file(); } // then we're done with all the config files release_config_file(files[0]); release_config_file(files[1]); release_config_file(files[2]); } } // @notes A final note on "scratch arenas". // // Often it is useful to use an arena for a temporary allocation that will be // thrown away by the end of the current scope. One could create a new arena // with MD_ArenaAlloc and later release it with MD_ArenaRelease, but this // sort of case is perfect for using the thread-local scratch pool. // // To get an arena for scratch work from the pool one uses: // MD_ArenaTemp scratch = MD_GetScratch(0, 0); // To allocate with it: // MD_WhateverArenaApi(scratch.arena, ...); // And to release it: // MD_ReleaseScratch(scratch); // // If an arena is being used for allocating something to return to the caller // it is important that it not also be the scratch, or else when the scratch // release happens, all of the memory that was supposed to stay allocated // for the caller to see will also be released (or worse, marked as released // but still valid looking for some time). // // To avoid this when getting a scratch arena the API MD_GetScratch allows you // to specify arenas you are already using, to force it to pick one you are // not using. In 99.99% of cases a call to this API either looks like: // MD_GetScratch(0, 0); // Or // MD_GetScratch(&arena, 1); // // If there are more than one arena already in use when a new scratch is // needed they can all be specified by packing them into an array: // MD_Arena *arena_conflicts[2] = {arena1, arena2}; // MD_GetScratch(arena_conflicts, 2); // // Watch out! If the scratch pool doesn't have a non-conflicting arena then // it will return a null handle, likely leading to a crash. This can be // avoided by defining a higher value for #define MD_IMPL_ScratchCount. // But it's generally possible and a lot better to avoid this path.