Question about realloc

let layout_arr = std::alloc::Layout::array::<i128>(10).unwrap();
    unsafe {
        let mut ptr_arr = std::alloc::alloc(layout_arr) as *mut i128;
       
        ptr_arr = std::alloc::realloc(ptr_arr as *mut u8, layout_arr, 100) as *mut i128;
    }

Do I have to assign the reallocated mem, or is that new allocated mem being pointed by the ptr_arr after realloc but without assignement, i.e:
std::alloc::realloc(ptr_arr as *mut u8, layout_arr, 100)
I read the docs and I see:

If this returns a non-null pointer, then ownership of the memory block referenced by ptr has been transferred to this allocator.

Tbh, ptr_arr is NOT an allocator.

If you just reassign, you may leak memory.

You're supposed to check first if the realloc succeeded, and only then replace the old pointer with the new. If realloc succeeds, the old pointer is invalid, freed, and must not be used any more.

1 Like

Thanks, so basically:
Pseudocode:
let new_ptr = std::alloc::realloc(ptr_arr as *mut u8, layout_arr, 100);

if new_ptr != nullptr
{
ptr_arr = new_ptr;
}

Is that correct?

Beware that size in realloc is in bytes, you probably didn't mean to realloc to 100 here. You should compute the total size for 100 elements instead.

Also, you should update layout_arr with new size in case realloc succeeded. This way, you can call dealloc with layout_arr wether realloc succeeds or not.

Thank you. That is really important (and appreciated) answer.
I strongly believe that the API for memory allocation in rust is messed up.
Realloc is confusing. Why not return in rust spirit, Result? Same alloc?
Also, to have to pass correct ptr + layout to dealloc? I can understand it, but it is actually more error prone than C/C++. I'm not sure if this is good.

Well, as the docs say it is "expected to be deprecated in favor of the alloc method of the Global type when it and the Allocator trait become stable."

C++ has multiple ways to allocate and deallocate memory. deallocate also requires you to pass the size.

And what changes in the API there are?
[Edit]
Ah, I see, they do return Result. Finally.

The overly short version of it is that you're not expected to be using raw allocation in Rust. You're expected to use Box or Vec or some other higher level container instead. The GlobalAlloc and allocation free functions were thus designed not to be used, but to fit the specific need of swapping out the allocation implementation underlying Box et. al.

That said, certain things do require doing manual allocation; I've written a decent amount of such code to enable some VLA use cases on stable Rust.

When you call realloc, it's semantically equivalent to allocating a new pointer and freeing the old one. If the realloc succeeds, then all accesses must go through the new pointer, not the old one. This is the case even if the address doesn't change.

The unstable Allocator trait used to have a grow_in_place method which would fail if a data relocation were to take place. It got removed because despite being guaranteed "in place" it still necessarily invalidated all old pointers.

This is the case even in C.

The original pointer ptr is invalidated and any access to it is undefined behavior (even if reallocation was in-place). [cppreference]

I agree this is suboptimal wording. "This allocator" does refer to the impl GlobalAlloc, but "ownership [...] has been transferred to this allocator" is at best vague. The next line of

The memory may or may not have been freed, and should be considered unusable.

helps, but I think it's probably better for the trait user if the trait docs would just state that the old pointer is invalidated/unusable.

What's being said is that the pointer no longer owns the memory block it pointed to, and the allocator has reclaimed ownership of that memory. This is I think a casualty of trait docs being written for two audiences: both for the consumer of the trait and the implementor of the trait.


When I'm in a project involving a decent amount of manual allocation, I'll typically write a couple of simple wrappers, roughly:

fn alloc(layout: Layout) -> NonNull<u8> {
    NonNull::new(std::alloc::alloc(layout))
        .unwrap_or_else(handle_alloc_error)
}

unsafe fn dealloc(ptr: NonNull<u8>, layout: Layout) {
    std::alloc::dealloc(ptr.as_ptr(), layout);
}

but sometimes also, when it's less dynamic,

fn alloc<T>() -> NonNull<u8> {
    let layout = Layout::new::<T>();
    previous::alloc(layout)
}

unsafe fn dealloc<T>(ptr: NonNull<T>) {
    let layout = Layout::new::<T>();
    previous::dealloc(ptr, layout);
}

but always, if I can get away with it, it's better to stick to using Box (or Rc, or another container type) to manage allocation, because you get unwind cleanup for free.

When you have pointer-soup ownership, you do need to use raw pointers throughout to avoid asserting Rust's strong borrowing semantics where not intended. But even if you're going to drop to raw pointers everywhere, it's still a good idea to start out with Box or whatever initially rather than raw allocation if you can do so.

If you're only dealing with Sized-typed allocation, the only reason to bypass Box is

  • if Box::new(MaybeUninit::uninit()) is failing to elide the copy / is blowing the stack, or
  • if pointer directionality is cyclic and can't coexist with a Box owner of the root.

(Though in the latter case do consider if cyclic weak reference counting is sufficient, along with Arc::new_cyclic designed for patterns like C++ enabled_shared_from_this where you hold a Weak<Self> member. In the former case, consider if Vec::with_capacity will work; you can turn Vec<T> into Box<[T; N]> without reallocating if .cap()==.len()==N. In either case you can get the benefit of automatically correct unwind cleanup, which is entirely manual for manual allocation.)

8 Likes

Hi and thank you for your post. I appreciate it.

I found it implausible. Those are if you please, primitives for low level operations and are accessible to the public.
They just have terrible API. That's all there is to it.

Yes, me too. And this is my peeve with the alloc module API. It should be defined and thought through that covers most of the every day (here every time you need to manually manage mem) situations and only rarely it needs to be wrapped in order to suit particular needs. But this isn't the case now. Basically, in order to have to have it (API) ergonomical one needs to write wrapper for the most obvious scenarios. Seriously, here there is two of us and both of us wrote more or less similar wrappers. But I'm sure there are thousands of others that do exactly the same! And this isn't right. Such functionality should be in std. I'm done... :slight_smile:

Thanks for your post.

Just to say it again, the purpose of the current API is to implement #[global_allocator]. That this enables manual dynamic allocation is a convenient side effect.

The "good" interface exists, but is unstable; the Allocator trait. It's still unstable because designing the "good" interface is nontrivial! At a minimum, it's unclear whether the Allocator trait is correct in always returning the slack space (i.e. the returned slice is longer than the requested size) or if that should be a separate method; supporting slack space may have additional overhead over not such that you only want to do it sometimes. It's also extremely unclear how any of this should interact with allocation elision. Additionally, there's the "storages" counter-proposal[1], which provides a slightly generalized API purporting to enable the use of "small vector" optimizations with standard collection types.

I'm not. Or at least, if the manitude is correct here, there are far more people writing Rust already than I thought. (And I'm already fairly enthusiastic in my estimation.)

The only time you need to use raw allocation is when you're working with custom DSTs[2] or otherwise working outside of the Rust type system. This is extremely uncommon. Rust essentially doesn't support these use cases, and using them requires falling back onto essentially writing C with Rust syntax.

Such uses (unless imposed on you by FFI, where you have bigger API woes than null-means-failure... or perhaps just more of that) are fundamentally optimizations over a simpler structure which doesn't mandate the use of unsafe, potentially via indexing, extra allocations, or some other use of standard container interfaces. I do get unreasonably exited to microoptimize data structures, but this is the exception rather than the norm; most code is perfectly capable of taking a bit less optimal solution for the ability to do so safely, in a manner where they don't need to worry about violating unsafe requirements.

Note also that my wrapper makes a big assumption: that you don't care about supporting resource exhaustion. Doing fallible allocation is a reason to bypass standard collection types, but this is not a fundamental limitation, but rather a temporary limitation due to unstable API where it's unclear how best to expose that functionality. Similarly with direct in-place heap initialization without creating a copy on the stack first.

Is the allocation API returning Result<NonNull<_>, ()> instead of *mut _ a better interface? Absolutely! But not such an impactful change that it outprioritizes other work, such as figuring out the aforementioned how best to handle fallible allocation in collections (which in turn impacts the design of the allocation APIs).

Additionally, std isn't intended to be exhaustive. A key design choice of Rust has been to make using 3rd party crates a breeze via cargo. Std's API gets frozen forever once stabilized, but a nicer-alloc crate can release a version two if it turns out its interface can be meaningfully improved.


  1. For which I'm essentially the champion, and really should get around to formally proposing the project group/initiative to make a final pitch on it... ↩︎

  2. The simplest case of a slice tail can be described as a Rust type but not directly created. In some cases, you can create them by making the struct generic and utilizing unsizing from [T; N] to [T]; when that's not possible, you need a solution like that implemented by my slice-dst crate. ↩︎

1 Like

Looks like a missing footnote?

Yes*.

*Actually the content of that footnote became not a footnote, but the latter mention of direct in-place heap initialization without creating a copy on the stack first. So more "removed" and "dangling" than "missing".

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.