When I learned zig (to write roc platforms) I learned about the different zig-allocators. So I think it is a pity, that the roc-zig-platforms do not use them, and instead use extern fn malloc
.
For my AoC platform, I tried to use the the zig GeneralPurposeAllocatorr: https://github.com/ostcar/aoc2023/blob/f657d50aa2b393b5dc26b49baeb6f740ef405908/platform/host.zig#L30-L50
This worked fine, until I had to free memory for Day 16 (my solution for day 16 seems very memory inefficient).
How would I write roc_dealloc
when roc does not call it with a size
argument? Must the platform save the size of every allocation? Is there a trick, how to do this? My native solution would be, to use a global hashmap. But this seems inefficient and would need a lock, if the platform runs in parallel.
Why does roc_realloc
has the argument old_size
? If it is the responsibility of the platform to save the size, then it could also be used for roc_realloc
.
What about the alignment
argument? It is ignored in all examples and it does not seem to be needed. In what situation should the platform use it?
How would I write roc_dealloc when roc does not call it with a size argument?
Yeah the current API is really annoying to use with anything other than C malloc
and free
as the implementation.
The only way I know to deal with it is to slightly expand each allocation so that you can store the size just in front of the allocated bytes. It's definitely a pity that the definition of roc_dealloc
forces this. I see this as a design mistake that we haven't fixed. I'm not aware of any strong reason to do it this way.
If we added a size
argument, we could make some compiler changes to pass it in from compiled Roc. It would also require changes to every platform that currently exists.
Why does roc_realloc has the argument old_size? If it is the responsibility of the platform to save the size, then it could also be used for roc_realloc.
Good point, I have no idea. As far as I can tell this is an old decision and undoing it involves a lot of friction because every single platform would have to be redone. I know there are other changes planned to the platform API (changing Effects into a sort of state machine). If we are making other changes then we'd definitely want to batch them together so that there's only one upgrade to do.
What about the alignment argument? It is ignored in all examples and it does not seem to be needed. In what situation should the platform use it?
Yeah I think in theory every roc_alloc
should ensure that the allocated memory has the requested alignment. The compiler is careful to ensure that Roc code supplies the right value. But nobody has written a platform yet that uses this argument. As far as I can tell, this is basically a bug in all our platforms that has been copied and pasted lots of times. But in practice maybe malloc
already aligns to 16 bytes or something and we never ask for more alignment than that so everything is OK? That might be different in a custom allocator.
we should be using aligned_alloc over malloc in examples but aren't :sweat_smile:
in C examples at least; in Rust ones we should be using Rust's global allocator, but that requires knowing the size when we dealloc
we want to pass that but currently we can't because seamless slices don't know it
there's a project to change that, but it requires changing how every List
and Str
are represented in memory - @Brendan Hansknecht knows more
But in practice maybe
malloc
already aligns to 16 bytes or something and we never ask for more alignment than that so everything is OK? That might be different in a custom allocator.
100% this. That so why roc alignment never matters currently.
As for passing size to dealloc. One simple solution would be to store an extra usize and the heap for every list and string. This is quite wasteful, especially given many allocators already store this information internal (that is why malloc doesn't need the information for example).
From the last chat Richard and I had about this, we probably want roughly this API:
fn roc_alloc(alignment: u8, size: usize, roc_tracked_sized: bool) -> *void;
fn roc_realloc(ptr: *void, alignment: u8, requested_size: usize, old_size: Option<usize>) -> *void;
fn roc_dealloc(ptr: *void, size: Option<usize>);
For all types that have a size that roc can determine at compile time (records, tags, etc), roc will pass the size in. For types with a size that we can't determine at compile time (list, str), roc won't pass in a size. Most importantly with this, roc will let the platform know this info. As such, if the platform uses malloc, which already records the size, the platform can just ignore this info. If the platform uses some other for of allocator that needs to know the size, it is the platforms job to store the size somewhere (probably right before the roc allocation) when roc can't know the size at compile time.
This minimizes repeated storage of the allocation size overall.
Otherwise, every single list and string would require and extra usize no matter the allocator due to seamless slices not knowing the original capacity of the allocation.
also note that in a future where Roc supports simd, 16 may not be enough anymore! So malloc happens to always work today, but may stop working in the future
Very true
This is wonderfully convenient, I came here to ask exactly this question after hitting this same issue trying to get a custom allocator to work for an embedded rust platform
Last updated: Jul 05 2025 at 12:14 UTC