Memory allocation

Hello,

I'm a structural engineer trying to learn Rust. I've purchased (3) different books, but this one concept is still unclear.
I have seen the expression

let s1 = String::from("hello");

graphically represented as follows:

when describing ownership. However, where in this graphic are the actual variable and data type registered? Are there additional pieces of information associated with this assignment that are not represented in the graphic?

Thanks in advance.

Where is s2 defined? I don't see why it is auto sharing the 'hello' repr with s1.

Ownership is an abstract concept, not something stored in memory. When a value is owned, it means the compiler will generate code that destroys it when it goes out of scope. If a value is borrowed, compiler will let it go out of scope without doing anything about it.

In Rust Box<Foo> and &Foo are 100% identical in memory. Exact same size, layout, bytes.

4 Likes

Although this graphic is describing ownership, my question more directed at trying to understand, with a graphical representation, what are all of the pieces of information that are tied to the expression, and where are they stored.

  1. the variable "s1" (where is it's info?)
  2. the data type (where is it's info?)
  3. the pointer (on the stack)
  4. the length (on the stack)
  5. the capacity (on the stack)
  6. the data itself (on the heap)

Items 1 and 2 above are not represented in the table shown in the graphic.

1 and 2 have a concrete existence only during compilation: They affect the code that the compiler generates, but do not appear explicitly in the final binary.

5 Likes

ok, that makes sense. thx.

I believe your questions have been answered, but here are entirely too many words motivated by what I perceived [1] prompted the questions.


It's showing the state of memory (conceptually anyway [2]) for

{
   let s1 = "hello".to_string();
   let s2 = s1;
   /* Diagram shows the state here, after assignment but before drops */
}

The assignment just copied over the pointer, length, and capacity. The compiler does flow analysis to know that s2 is now the owner and s1 is "dead" -- you can't use it anymore. Since you can't use it, it is ok that it is still pointing to the same memory location. When the block exits, s2's destructor will run, freeing the memory. s1's will not; it is no longer the owner.

If you did this instead:

let mut s1 = "hello".to_string();
s1 = "another string".to_string();

The the reassignment of s1 will destruct the first string, and s1's destruct will drop the second string.

In more complicated scenarios, where the compiler cannot figure out if an object needs to drop or not, it will generate a binary flag for the objects that need checked when the drop scope exits. Drop is a big topic, but here's a few short examples.

The compiler remembers these bits, but conceptually throws that information out after compiling things down to a point where it doesn't need the high-level information. Rust is a statically typed language, which means the type of every variable [3] is known at compile time and doesn't change at run time, so there's no need to store it dynamically (at run time). It doesn't have dynamic evaluation / interpretation either, so there's no need to keep the variable names around.

I say conceptually, as there are various scenarios where the information is retained in some way. Some examples:

  • If you did something explicit like dbg!(s1), the name of the variable will be baked into the program so that the debug output can be printed
  • Similarly if you call type_name, say
  • Binary debug info (implicitly), so a debugger can connect a running program back to your source code
  • If you make use of the Any trait, you can check for type equality at run time [4], and this can be used to emulate dynamic typing

But in the general case, at the language level, there's no reason to keep a variable name or type in the binary.


There are various parts of the language that sometimes seem like there is dynamic typing, such as automatic coercions, e.g. from &String to &str [5]. But these are operations on the values that result in a new value with a different type (even in cases where the values consist of the same bits).

Rust does have type erasure (dyn Trait) as well, but note that

  • dyn Trait are dynamically sized and perform dynamic dispatch, but are still their own, singular, statically-known type
  • That is, type erasure doesn't dynamically change a type -- it's a type of coercion
  • Downcasting is done via the Any trait, and variables still don't change type
  • We'll probably get more coercions between related dyn Trait types, but it will still be a conversion of values to new types

  1. perhaps incorrectly ↩︎

  2. very basic optimization can remove the assignment ↩︎

  3. and expression ↩︎

  4. something that identifies the type of values is compiled into the binary ↩︎

  5. subtyping of lifetimes is another example ↩︎

5 Likes

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.