Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Overview
Addresses the proposal in #372.
It indeed took a while until I got around to implement it and this change turned out to be more involved than I expected...
Disclaimer
I am mainly a C++ developer and only rarely write pure C. I tried to stick to the coding style / naming conventions / etc. used throughout MIR but I might have missed some things. You have been warned :-)
Implementation
The general purpose memory management routines (
malloc
,calloc
,realloc
,free
) are bundled in structMIR_alloc
, which is just a collection of function pointers and some user data. Similarly, function pointers to executable code related memory management functions (mem_map
,mem_unmap
,mem_protect
) are bundled inMIR_code_alloc
.Users wanting to provide custom allocators can do so by calling newly introduced function
MIR_init2
(name to be discussed), which takes pointers to both aMIR_alloc
and aMIR_code_alloc
. Both can beNULL
, which results in a default implementation being used. Both pointers are persisted in theMIR_context
(fieldsalloc
andcode_alloc
) for easy consumption by other functions.The following convenience functions are provided to work with allocators and replace the "raw" functions of corresponding name:
void *MIR_malloc (MIR_alloc_t alloc, size_t size)
void *MIR_calloc (MIR_alloc_t alloc, size_t num, size_t size)
void *MIR_realloc (MIR_alloc_t alloc, void *ptr, size_t old_size, size_t new_size)
(a little different than plainrealloc
, see next section)void MIR_free (MIR_alloc_t alloc, void *ptr)
Similarly, for the executable code management functions:
void *MIR_mem_map (MIR_code_alloc_t code_alloc, size_t len)
int MIR_mem_unmap (MIR_code_alloc_t code_alloc, void *ptr, size_t len)
int MIR_mem_protect (MIR_code_alloc_t code_alloc, void *ptr, size_t len, MIR_mem_protect_t prot)
I initially planned to only feed
MIR_alloc
into theMIR_context
and feedMIR_code_alloc
instead intoMIR_gen_init
, but this turned out to be impossible without yet more involved refactorings.As it currently stands, the core API (exposed by
mir.h
,mir-gen.h
, etc.) did not change, i.e. existing projects should compile and run just like before without requiring any changes. Internal APIs (likemir-varr.h
,mir-htab.h
, etc.) did however change, though I tried to keep these to a minimum. More on this in the next section.Potentially Controversial Design Decision
VARR
s keep a pointer to their allocatorBoth
VARR
s andHTAB
s (as well as bitmaps) now require passing an allocator when calling their create functions. I wanted to avoid also passing allocators when invoking other operations that might need to manage memory (push, expand, destroy, ...), as this would have severly cluttered the API.Instead, I opted for
VARR
s storing an additional pointer to the allocator which was passed in when creating them, which is then used for further operations. Slightly increases their memory footprint though.realloc
takes the previous size as an additional parameterAlternative title: C's and C++'s allocation APIs are not friends.
As I mentioned initially, I come from a C++ background. When designing the allocator interface for MIR, I tried to do it in a way that would also play somewhat nicely with what C++ has to offer. Sadly, what C++ has to offer is less than spectacular:
std::allocator
is a complete mess and its intended replacementstd::pmr::polymorphic_allocator
/std::pmr::memory_resource
shares some of its quirks:realloc
-style operationdeallocate
To support as wide a range of allocators as possible without complicating the API too much, I opted for the following compromise:
MIR_realloc
takes the previous size as an additional parameter. This allows allocator implementations to translate calls toMIR_realloc
into aallocate
-memcpy
-free
sequence. AsMIR_realloc
is only called insidemir-varr.h
, required changes on MIR's part were minimal.MIR_free
on the other hand does not take the previous size. This would have required a lot of changes on MIR's part as there are a decent amount of allocations whose size is dynamically determined at runtime. Users wishing to use an API requiring this will need to do their own bookkeeping.Of course, we could always go more into one or the other direction and:
MIR_realloc
MIR_alloc
/MIR_free
, requiringVARR
to always copy manually upon expansionDocumentation
I added some documentation, both for users of MIR as a library as well as developers of MIR, in
CUSTOM-ALLOCATORS.md
.Testing
This is partly covered by the existing tests, though it may be nice to add some allocator specific ones. While it would be pretty straightforward to test that a given allocator has been called, the other way around (ensuring that
malloc
,free
, etc. are not called when a custom allocator is passed) is a bit harder. Not sure if there is some cross platform magic to overridemalloc
and the others...Things I am unsure about
Conclusion
I hope this fits MIR and my ramblings did not scare you away already :-)
Let me know if there are things that need improvement / polish.