Skip to content

Commit

Permalink
Merge pull request #413 from ningvin/custom-allocators-squash
Browse files Browse the repository at this point in the history
Add support for custom allocators
  • Loading branch information
vnmakarov authored Aug 9, 2024
2 parents d6dffef + 3d6f36b commit 8ce5a06
Show file tree
Hide file tree
Showing 34 changed files with 1,130 additions and 586 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
*.a
/mir-run
/build
/.cache
198 changes: 198 additions & 0 deletions CUSTOM-ALLOCATORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# Custom Allocators

In some environments, memory cannot / should not directly be managed by calls to `malloc`, `free` etc. for various reasons. To support this use case, MIR lets you provide user defined allocators. These can be supplied during context creation by calling `MIR_context_t MIR_init2 (MIR_alloc_t alloc, MIR_code_alloc_t code_alloc)`.

Calling `MIR_context_t MIR_init (void)` instead without passing custom allocators will default to using the standard functions `malloc`, `free`, ..., as well as the operating systems default routines for memory mapping and protection.

## User Guide

The following sections are intended for users of MIR as a library. If you want to contribute to MIR directly, take a look at [the developer guide](#developer-guide).

### General Purpose Allocators

`MIR_alloc` is the general purpose allocator type defined by MIR, used for most allocations. Users wishing to provide a general prupose allocator need to define the following functions:

- `void *malloc (size_t size, void *user_data)`
- `void *calloc (size_t num, size_t size, void *user_data)`
- `void *realloc (void *ptr, size_t old_size, size_t new_size, void *user_data)`
- `void free (void *ptr, void *user_data)`

These functions should follow the same semantics as the standard C functions of the same name. This includes the platform's alignment guarantees.

> [!IMPORTANT]
> The `realloc` function required by `MIR_alloc` slightly differs from its standard C counterpart in that it takes an additional parameter `old_size`, which denotes the size of the allocation `realloc` is invoked on.
> This was introduced to support allocators that do not provide `realloc` natively, as shown in [the example below](#example).
> Allocators that do support `realloc` out of the box can ignore this parameter or use it for validation purposes.
> [!IMPORTANT]
> Some allocator implementations (such as `std::pmr::(un)synchronized_pool_resource` in libstd++ / libc++) require users to provide the exact size of the allocation to calls of their deallocation function.
> This approach turns out to be largely infeasible for MIR as there are countless allocations whose size is dynamically determined, which would (in contrast to the `realloc` compromise outlined above) require a lot of additional bookkeeping on MIR's part.
> Users wishing to use such an allocator with MIR may need to implement this additional bookkeeping themselves.
Apart from the pointers and sizes one would expected, all functions additionally accept a `user_data` parameter. This can be used to pass additional context as outlined in [the example below](#example).

> [!WARNING]
> The `MIR_alloc` instance passed to `MIR_init2` must have a lifetime greater or equal to the resulting `MIR_context`, i.e. live at least as long as the subsequent call to `MIR_finish`.
> The `MIR_alloc` instance being destroyed or going out of scope beforehand may result in undefined behavior.
### Executable Code Allocators

`MIR_code_alloc` is the executable code related allocator type defined by MIR. It is used to map and unmap pages of memory, as well as manipulate their protection. Users wishing to provide an executable code allocator need to define the following functions:

- `void *(*mem_map) (size_t len, void *user_data)`: allocate and zero `len` bytes of memory (see `mmap` / `VirtualAlloc`)
- `int (*mem_unmap) (void *ptr, size_t len, void *user_data)`: free `len` bytes of memory at `ptr`, previously allocated by a call to `mem_map` (see `munmap` / `VirtualFree`)
- `int (*mem_protect) (void *ptr, size_t len, MIR_mem_protect_t prot, void *user_data)`: change the protection of memory identified by `ptr` and `len` according to the flags specified in `prot` (see `mprotect` / `VirtualProtect`)

Possible values for `prot` are contained in enum `MIR_mem_protect_t` (`PROT_READ_EXEC` and `PROT_WRITE_EXEC`).

Similar to `MIR_alloc`, `MIR_code_alloc` lets users pass `user_data` to the different functions.

MIR will not try to directly write to or execute memory returned by `mem_map`, but will instead call `mem_protect` with appropriate flags beforehand.

> [!WARNING]
> The `MIR_code_alloc` instance passed to `MIR_init2` must have a lifetime greater or equal to the resulting `MIR_context`, i.e. live at least as long as the subsequent call to `MIR_finish`.
> The `MIR_code_alloc` instance being destroyed or going out of scope beforehand may result in undefined behavior.
### Thread Safety

Users intending to use custom allocators while calling MIR functions from different threads need to ensure that their provided functions are thread safe.

### Example

This example showcases an approach to wrap a given stateful allocator interface, `my_allocator`, for use with MIR.

It uses some C++11/14 features, but can be easily adapted to work with older C++ standards.

```cpp
#include <cstddef>
#include <cstdint>
#include <cstring>

#include "mir.h"

template<typename T>
inline constexpr T align(T value, uint64_t alignment)
{
// sadly `std::align` is only useful for very specific use cases,
// hence we roll our own alignment routine:
return (T) ((((uint64_t) value) + alignment - 1) & ~(alignment - 1));
}

class my_allocator
{
public:
virtual ~my_allocator() = default;
void *allocate(size_t size) = 0;
void deallocate(void *ptr) = 0;
};

class context
{
public:
context(my_allocator &allocator)
: _allocator{allocator}
, _mir_alloc{&context::do_malloc,
&context::do_calloc,
&context::do_realloc,
&context::do_free,
this} // user_data
, _mir_context{MIR_init2(&_mir_alloc, nullptr)}
{
}

~context()
{
if (_mir_context != nullptr)
{
MIR_finish(_mir_context);
}
}

// ...

private:
static context &context_from_user_data(void *user_data)
{
return *static_cast<context *>(user_data);
}

static void *do_malloc(size_t size, void *user_data)
{
auto &self = context_from_user_data(user_data);
return self._allocator.allocate(size);
}

static void *do_calloc(size_t num, size_t size, void *user_data)
{
auto &self = context_from_user_data(user_data);
const size_t aligned_size = align(size, alignof(std::max_align_t));
const size_t total_size = aligned_size * num;
void *const ptr = self._allocator.allocate(total_size);
std::memset(ptr, 0, total_size);
return ptr;
}

static void *do_realloc(void *ptr, size_t old_size, size_t new_size, void *user_data)
{
auto &self = context_from_user_data(user_data);
void *const new_ptr = self._allocator.allocate(size);
// if the `my_alloctor` interface supports a `realloc` method natively,
// we could simply call it here;
// instead, for the purpose of this example, we have to rely on the size
// of the previous allocation to be able to translate `realloc` into
// `allocate` - `memcpy` - `deallocate`:
std::memcpy(new_ptr, ptr, old_size);
self._allocator.deallocate(ptr);
return new_ptr;
}

static void do_free (void *ptr, void *user_data)
{
if (ptr == nullptr)
{
return;
}
auto &self = context_from_user_data(user_data);
self._allocator.deallocate(ptr);
}

private:
my_allocator &_allocator;
MIR_alloc _mir_alloc;
MIR_context_t _mir_context;
};
```
## Developer Guide
The following sections are intended for contributors to MIR.
### Overview
Pointers to allocators are stored in fields `alloc` and `code_alloc` of struct `MIR_context`. These pointers are always valid, even if the user did not provide any or only some allocators explicitly (in this case, default allocators are used where needed).
Passing the executable code allocator only to `MIR_gen_init` may seem conceptually more sound, but does not seem to work in practice as the interpreter relies on some of the code generation infrastructure as well.
The vector implementation in [`mir-varr.h`](mir-varr.h) keeps an additional pointer to the allocator it was created with. While this slightly increases its memory footprint, the alternative (passing a `MIR_alloc_t` to each and every of its operations) made for a very verbose API.
### Executables shipped with MIR
Custom allocators are mostly relevant for uses of MIR as a library in some other project. In case you are working on some executable specific part of MIR, e.g. tests, you can mostly ignore custom allocators and simply call `MIR_init` instead of `MIR_init2` as before.
In case you are testing / using some of the lower level APIs that require you to explicitly pass an allocator, such as the [`VARR`](mir-varr.h) or [`HTAB`](mir-htab.h) implementations, you can include [`mir-alloc-default.c`](mir-alloc-default.c) into your translation unit and simply pass `&default_alloc` where required. The same goes for code allocators and [`mir-code-alloc-default.c`](mir-code-alloc-default.c) / `&default_code_alloc` respectively.
### MIR as a Library
Code shipped as part of the main MIR library should avoid calling standard memory management routines such as `malloc`, `free`, `mmap`, ... directly and instead use the following allocator aware replacements (located in [`mir-alloc.h`](mir-alloc.h) and [`mir-code-alloc.h`](mir-code-alloc.h) respectively):
- `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)`
- `void MIR_free (MIR_alloc_t alloc, void *ptr)`
- `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)`
Suitable allocators can usually be obtained directly from the `MIR_context` (fields `alloc` and `code_alloc`), or by calling `MIR_alloc_t MIR_get_alloc (MIR_context_t ctx)`.
In case no `MIR_context` is available in a function where you require an allocator (neither directly nor indirectly through other sub-contexts such as `gen_ctx_t`), consider taking a `MIR_alloc_t` (or `MIR_code_alloc_t`) as a parameter.
5 changes: 3 additions & 2 deletions MIR.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
* MIR API code has an implicit state called by MIR context
* MIR context is represented by data of `MIR_context_t`
* MIR context is created by function `MIR_context_t MIR_init (void)`
* Every MIR API function (except for `MIR_init`) requires MIR context passed through the first argument of type `MIR_context_t`
* In case you want to use custom allocators, use `MIR_context_t MIR_init2 (MIR_alloc_t, MIR_code_alloc_t)` instead (see [here](CUSTOM-ALLOCATORS.md) for more details)
* Every MIR API function (except for `MIR_init` / `MIR_init2`) requires MIR context passed through the first argument of type `MIR_context_t`
* You can use MIR functions in different threads without any synchronization
if they work with different contexts in each thread

## MIR program
* MIR program consists of MIR **modules**
* To start work with MIR program, you should first call API function `MIR_init`
* To start work with MIR program, you should first call API function `MIR_init` / `MIR_init2`
* API function `MIR_finish (MIR_context_t ctx)` should be called last. It frees all internal data used to work with MIR program and all IR (insns, functions, items, and modules) created in this context
* API function `MIR_output (MIR_context_t ctx, FILE *f)` outputs MIR textual representation of the program into given file
* API function `MIR_scan_string (MIR_context_t ctx, const char *str)` reads textual MIR representation given by a string
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
## Disclaimer
* **There is absolutely no warranty that the code will work for any tests except ones given here and on platforms
other than x86_64 Linux/OSX, aarch64 Linux/OSX(Apple M1), and ppc64le/s390x/riscv64 Linux**

## MIR
* MIR is strongly typed IR
* MIR can represent machine 32-bit and 64-bit insns of different architectures
Expand Down Expand Up @@ -178,7 +178,7 @@ ex100: func v, 0
* Running code from the above example could look like the following (here `m1` and `m2` are modules
`m_sieve` and `m_e100`, `func` is function `ex100`, `sieve` is function `sieve`):
```c
/* ctx is a context created by MIR_init */
/* ctx is a context created by MIR_init / MIR_init2 */
MIR_load_module (ctx, m1); MIR_load_module (ctx, m2);
MIR_load_external (ctx, "printf", printf);
MIR_link (ctx, MIR_set_interp_interface, import_resolver);
Expand Down Expand Up @@ -316,8 +316,8 @@ The executable is "configurable" with environment variables:
* Files `mir-gen-x86_64.c`, `mir-gen-aarch64.c`, `mir-gen-ppc64.c`, `mir-gen-s390x.c`,
and `mir-gen-riscv64.c` is machine dependent code of JIT compiler
* Files `mir-<target>.c` contain simple machine dependent code common for interpreter and
JIT compiler
* Files `mir-<target>.h` contain declarations common for interpreter and JIT compiler
JIT compiler
* Files `mir-<target>.h` contain declarations common for interpreter and JIT compiler
* Files `mir2c/mir2c.h` and `mir2c/mir2c.c` contain code for MIR to C compiler. The generated code might be not portable
* Files `c2mir/c2mir.h`, `c2mir/c2mir.c`, `c2mir/c2mir-driver.c`, and `c2mir/mirc.h` contain code for
C to MIR compiler. Files in directories `c2mir/x86_64` and `c2mir/aarch64`, `c2mir/ppc64`, `c2mir/s390x`,
Expand Down Expand Up @@ -346,7 +346,7 @@ The executable is "configurable" with environment variables:
[1] is based on wall time of compilation of C sieve code (w/o any include file and with
using memory file system for GCC) and the corresponding MIR sieve code by MIR-interpreter
and MIR-generator with optimization level 2

[2] is based on the best wall time of 10 runs with used MIR-generator optimization level 2

[3] is based on stripped sizes of cc1 for GCC and MIR core and interpreter or generator for MIR
Expand Down Expand Up @@ -388,7 +388,7 @@ The executable is "configurable" with environment variables:
* wasi LLVM is a C to webassember clang compiler (11.0.0) with wasmer (1.0.2) based on LLVM backend
* wasi singlepass is a C to webassember clang compiler (11.0.0) with wasmer (1.0.2) based on singlepass backend
* wasi wasmtime is a C to webassember clang compiler (11.0.0) with wasmtime (0.26.0) runtime based on cranelift backend

| | Average | Geomean |
|--------------------------------------------------|-----------|-----------|
| gcc -O2 | 1.00 | 1.00 |
Expand Down
12 changes: 8 additions & 4 deletions adt-tests/mir-bitmap-test.c
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#include "mir-bitmap.h"
#include "mir-alloc.h"

#include "mir-alloc-default.c"

int main (void) {
MIR_alloc_t alloc = &default_alloc;
int status;
bitmap_t b1, b2, b3, b4;

b1 = bitmap_create ();
b2 = bitmap_create ();
b3 = bitmap_create ();
b4 = bitmap_create ();
b1 = bitmap_create (alloc);
b2 = bitmap_create (alloc);
b3 = bitmap_create (alloc);
b4 = bitmap_create (alloc);
status = bitmap_empty_p (b1);
status &= bitmap_bit_count (b1) == 0;

Expand Down
8 changes: 7 additions & 1 deletion adt-tests/mir-htab-test.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
#include "mir-htab.h"
#include "mir-alloc.h"

#include "mir-alloc-default.c"

static int status = 1;

Expand All @@ -24,11 +27,14 @@ static void add (int i, void *arg) {
(*sum) += i;
}



int main (void) {
MIR_alloc_t alloc = &default_alloc;
int i, collisions, iter, tab_el;
HTAB (int) * htab;

HTAB_CREATE_WITH_FREE_FUNC (int, htab, 4, hash, eq, f, ARG);
HTAB_CREATE_WITH_FREE_FUNC (int, htab, alloc, 4, hash, eq, f, ARG);
status &= HTAB_ELS_NUM (int, htab) == 0;
for (iter = 0; iter < 10; iter++) {
for (i = 0; i < 100; i++) {
Expand Down
14 changes: 9 additions & 5 deletions adt-tests/mir-reduce-test.c
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#include <stdio.h>
#include "mir-alloc.h"
#include "mir-reduce.h"
#include "mir-varr.h"
#include "real-time.h"

#include "mir-alloc-default.c"

DEF_VARR (uint8_t);
static VARR (uint8_t) * orig, *buf1, *buf2;

Expand Down Expand Up @@ -43,25 +46,26 @@ static size_t writer2 (const void *start, size_t len, void *aux_data) {
}

int main (int argc, const char *argv[]) {
MIR_alloc_t alloc = &default_alloc;
size_t i, n;
double start = real_usec_time ();

if (argc != 2 || (input_file = fopen (argv[1], "rb")) == NULL) {
fprintf (stderr, "usage: %s <inputfile>\n", argv[0]);
return 1;
}
VARR_CREATE (uint8_t, orig, 0);
VARR_CREATE (uint8_t, buf1, 0);
if (!reduce_encode (reader1, writer1, NULL)) {
VARR_CREATE (uint8_t, orig, alloc, 0);
VARR_CREATE (uint8_t, buf1, alloc, 0);
if (!reduce_encode (alloc, reader1, writer1, NULL)) {
fprintf (stderr, "Error in reducing input file!\n");
return 1;
}
fprintf (stderr, "Compression: original len = %llu, result = %llu, ration=%.2f, time=%.2fms\n",
(unsigned long long) input_length1, (unsigned long long) output_length1,
(input_length1 + 0.0) / output_length1, (real_usec_time () - start) / 1000.0);
VARR_CREATE (uint8_t, buf2, 0);
VARR_CREATE (uint8_t, buf2, alloc, 0);
start = real_usec_time ();
if (!reduce_decode (reader2, writer2, NULL)) {
if (!reduce_decode (alloc, reader2, writer2, NULL)) {
fprintf (stderr, "Corrupted input file!\n");
return 1;
}
Expand Down
6 changes: 5 additions & 1 deletion adt-tests/mir-varr-test.c
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
#include "mir-alloc.h"
#include "mir-varr.h"

#include "mir-alloc-default.c"

DEF_VARR (int);
int main (void) {
MIR_alloc_t alloc = &default_alloc;
int status, elem;
VARR (int) * test;
size_t ind;
int arr[] = {1, 2, 3};

VARR_CREATE (int, test, 0);
VARR_CREATE (int, test, alloc, 0);
status = VARR_LENGTH (int, test) == 0;
VARR_PUSH (int, test, 42);
status &= VARR_LAST (int, test) == 42;
Expand Down
Loading

0 comments on commit 8ce5a06

Please sign in to comment.