Advice on implementing a garbage collector in Rust - pointers

I'm working on a toy programming JavaScript-like language which I'm writing in Rust, and this language has a very basic mark and sweep garbage collector. I have a working prototype, and the way I implemented it is to wrap Rust objects which are going to be allocated on the GC heap inside of a Box. Then I can get a pointer to the GC'd object and wrap pointer into a dynamically-typed Value type, i.e.:
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum Value
{
Int64(i64),
UInt64(u64),
HostFn(HostFn),
Fun(*mut Function),
Str(*mut String),
Nil,
}
These are the values which my bytecode interpreter works with. The full source code is here if anyone wants to see more about how the GC works and provide feedback. I'm trying to keep the implementation as simple as possible so that this will be beginner-friendly.
What I would like advice with is that working with mutable pointers like this in Rust is very unergonomic. Every time I want to dereference them, I have to wrap everything in unsafe. This means, if I want to implement string concatenation or equality, I have to wrap the whole thing in an unsafe block where I'll dereference and borrow both strings. I have to do the same thing if I want to implement function calls. That's going to result in unsafe blocks everywhere in the implementation of my interpreter.
So my question is: can we make this more ergonomic somehow? One potential idea would be to have a helper method on Value that can return a &'static mut String for example. This is essentially lying to the Rust compiler about lifetimes for the sake of convenience, because the programmer still needs to be careful that GC'd values are on the VM heap, otherwise they will be collected. Would this be safe though? The real danger would come if the Rust compiler were to reorder operations around. Advice welcome.

Related

Can I define a pointer in julia?

How do I define a pointer to a variable or list element in Julia? I have tried reading some resources but I am really confused about using a pointer in Julia.
You cannot have a pointer to a variable—unlike C/C++, Julia doesn't work like that: variables don't have memory locations as part of the language semantics; the compiler may store a variable in memory (or in a register), but that's none of your business 😁. Mutable objects, however, do generally live in memory and you can get a pointer to such an object using the pointer_from_objref function:
pointer_from_objref(x)
Get the memory address of a Julia object as a Ptr. The existence of
the resulting Ptr will not protect the object from garbage collection,
so you must ensure that the object remains referenced for the whole
time that the Ptr will be used.
This function may not be called on immutable objects, since they do
not have stable memory addresses.
See also: unsafe_pointer_to_objref.
Why the awful name? Because, really why are you taking pointers to objects? Probably don't do that. You can also get a pointer into an array using the pointer function:
pointer(array [, index])
Get the native address of an array or string, optionally at a given
location index.
This function is "unsafe". Be careful to ensure that a Julia reference
to array exists as long as this pointer will be used. The GC.#preserve
macro should be used to protect the array argument from garbage
collection within a given block of code.
Calling Ref(array[, index]) is generally preferable to this function
as it guarantees validity.
This is a somewhat more legitimate use case, especially for interop with C or Fortran, but be careful. The interaction between raw pointers and garbage collection is tricky and dangerous. If you're not doing interop then think hard about why you need pointers—you probably want to approach the problem differently.

How should I decide when it is more or less appropriate to use raw pointers?

I get the impression that Rust is intended to be used in highly safe systems. Then I noticed that raw pointers allow arbitrary pointer arithmetic, and they can cause memory safety and security issues.
Basically, a pointer is an object that refers to another object. In most programming languages (I guess) a pointer is actually just a number that refers to a memory address. Rust's raw pointers are really just that - memory addresses. There are other pointer types in Rust (& references, Box, Rc, Arc), for which the compiler can verify that the memory is valid and contains what the program thinks it contains. This is not the case for raw pointers; they can in principle point to any memory location, regardless of the content. Refer to The Book for more details.
Raw pointers can only be dereferenced inside unsafe blocks. These blocks are a way for the programmer to tell the compiler "I know better than you that this is safe and I promise not to do anything stupid".
It is generally best to avoid raw pointers if possible because the compiler cannot reason about their validity, which makes them unsafe in general. Things that make raw pointers unsafe are the potential to...
access a NULL pointer,
access a dangling (freed or invalid) pointer,
free a pointer multiple times,
All these points boil down to dereferencing the pointer. That is, to use the memory pointed to.
However, using raw pointers without dereferencing them is perfectly safe. This has a use case in finding out if two references point to the same object:
fn is_same(a: &i32, b: &i32) -> bool {
a as *const _ == b as *const _
}
Another use case is the foreign function interface (FFI). If you wrap a C function that takes raw pointers as arguments, there is no way around providing them to the function. This is actually unsafe (as is the whole FFI business), because the function is likely to dereference the pointer. This means you are responsible for making sure the pointer is valid, stays valid, and is not freed multiple times.
Finally, raw pointers are used for optimization. For example, the slice iterator uses raw pointers as internal state. This is faster than indices because they avoid range checks during iteration. However, it is also unsafe as far as the compiler is concerned. The library author needs to pay extra attention, so using raw pointers for optimization always comes at the risk of introducing memory bugs that you normally do not have in rust.
In summary, the three main uses of raw pointers are:
"just numbers" - you never access the memory they point to.
FFI - you pass them outside Rust.
memory-mapped I/O - to trigger I/O actions you need to access hardware registers at fixed addresses.
performance - they can be faster than other options, but the compiler won't enforce safety.
As to when raw pointers should be used, the first three points are straight-forward: You will know when they apply because you have to. The last point is more subtle. As with all optimizations, only use them when the benefit outweighs the effort and risk of using them.
A counter example when not to use raw pointers is whenever the other pointer types (& references, Box, Rc, Arc) do the job.

Should I define my funcs with a pointer to the struct or just on the struct?

In go I seem to have two options:
foo := Thing{}
foo.bar()
foo := &Thing{}
foo.bar()
func (self Thing) bar() {
}
func (self *Thing) bar() {
}
What's the better way to define my funcs with self Thing or with self *Thing?
Edit: this is not a duplicate of the question about methods and functions. This question has to do with Thing and &Thing and I think it's different enough to warrent it's own url.
Take a look at this item from the official FAQ:
For programmers unaccustomed to pointers, the distinction between
these two examples can be confusing, but the situation is actually
very simple. When defining a method on a type, the receiver (s in the
above examples) behaves exactly as if it were an argument to the
method. Whether to define the receiver as a value or as a pointer is
the same question, then, as whether a function argument should be a
value or a pointer. There are several considerations.
First, and most important, does the method need to modify the
receiver? If it does, the receiver must be a pointer. (Slices and maps
act as references, so their story is a little more subtle, but for
instance to change the length of a slice in a method the receiver must
still be a pointer.) In the examples above, if pointerMethod modifies
the fields of s, the caller will see those changes, but valueMethod is
called with a copy of the caller's argument (that's the definition of
passing a value), so changes it makes will be invisible to the caller.
By the way, pointer receivers are identical to the situation in Java,
although in Java the pointers are hidden under the covers; it's Go's
value receivers that are unusual.
Second is the consideration of efficiency. If the receiver is large, a
big struct for instance, it will be much cheaper to use a pointer
receiver.
Next is consistency. If some of the methods of the type must have
pointer receivers, the rest should too, so the method set is
consistent regardless of how the type is used. See the section on
method sets for details.
For types such as basic types, slices, and small structs, a value
receiver is very cheap so unless the semantics of the method requires
a pointer, a value receiver is efficient and clear.
There isn't a clear answer but they're completely different. When you don't use a pointer you 'pass by value' meaning the object you called it on will be immutable (modifying a copy), when you use the pointer you 'pass by reference'. I would say more often you use the pointer variety but it is completely situational, there is no 'better way'.
If you look at various programming frameworks/class libraries you will see many examples where the authors have deliberately chosen to do things by value or reference. For example, in C# .NET this is the fundamental difference between a struct and a class and types like Guid and DateTime were deliberately implemented as structs (value type). Again, I think the pointer is more often the better choice (if you look through .NET almost everything is a class, the reference type), but it definitely depends on what you wish to achieve with the type and/or how you want consumers/other developers to interact with it. Your may need to consider performance and concurrency (maybe you want everything to be by value so you don't have to worry about concurrent ops on a type, maybe you need a pointer because the objects memory footprint is large and copying it would make your program too slow or consumptive).

How does Rust achieve compile-time-only pointer safety?

I have read somewhere that in a language that features pointers, it is not possible for the compiler to decide fully at compile time whether all pointers are used correctly and/or are valid (refer to an alive object) for various reasons, since that would essentially constitute solving the halting problem. That is not surprising, intuitively, because in this case, we would be able to infer the runtime behavior of a program during compile-time, similarly to what's stated in this related question.
However, from what I can tell, the Rust language requires that pointer checking be done entirely at compile time (there's no undefined behavior related to pointers, "safe" pointers at least, and there's no "invalid pointer" or "null pointer" runtime exception either).
Assuming that the Rust compiler doesn't solve the halting problem, where does the fallacy lie?
Is it the case that pointer checking isn't done entirely at compile-time, and Rust's smart pointers still introduce some runtime overhead compared to, say, raw pointers in C?
Or is it possible that the Rust compiler can't make fully correct decisions, and it sometimes needs to Just Trust The Programmer™, probably using one of the lifetime annotations (the ones with the <'lifetime_ident> syntax)? In this case, does this mean that the pointer/memory safety guarantee is not 100%, and still relies on the programmer writing correct code?
Another possibility is that Rust pointers are non-"universal" or restricted in some sense, so that the compiler can infer their properties entirely during compile-time, but they are not as useful as e. g. raw pointers in C or smart pointers in C++.
Or maybe it is something completely different and I'm misinterpreting one or more of { "pointer", "safety", "guaranteed", "compile-time" }.
Disclaimer: I'm in a bit of a hurry, so this is a bit meandering. Feel free to clean it up.
The One Sneaky Trick That Language Designers Hate™ is basically this: Rust can only reason about the 'static lifetime (used for global variables and other whole-program lifetime things) and the lifetime of stack (i.e. local) variables: it cannot express or reason about the lifetime of heap allocations.
This means a few things. First of all, all of the library types that deal with heap allocations (i.e. Box<T>, Rc<T>, Arc<T>) all own the thing they point to. As a result, they don't actually need lifetimes in order to exist.
Where you do need lifetimes is when you're accessing the contents of a smart pointer. For example:
let mut x: Box<i32> = box 0;
*x = 42;
What is happening behind the scenes on that second line is this:
{
let box_ref: &mut Box<i32> = &mut x;
let heap_ref: &mut i32 = box_ref.deref_mut();
*heap_ref = 42;
}
In other words, because Box isn't magic, we have to tell the compiler how to turn it into a regular, run of the mill borrowed pointer. This is what the Deref and DerefMut traits are for. This raises the question: what, exactly, is the lifetime of heap_ref?
The answer to this is in the definition of DerefMut (from memory because I'm in a hurry):
trait DerefMut {
type Target;
fn deref_mut<'a>(&'a mut self) -> &'a mut Target;
}
Like I said before, Rust absolutely cannot talk about "heap lifetimes". Instead, it has to tie the lifetime of the heap-allocated i32 to the only other lifetime it has on hand: the lifetime of the Box.
What this means is that "complicated" things don't have an expressible lifetime, and thus have to own the thing they manage. When you convert a complicated smart pointer/handle into a simple borrowed pointer, that is the moment that you have to introduce a lifetime, and you usually just use the lifetime of the handle itself.
Actually, I should clarify: by "lifetime of the handle", I really mean "the lifetime of the variable in which the handle is currently being stored": lifetimes are really for storage, not for values. This is typically why newcomers to Rust get tripped up when they can't work out why they can't do something like:
fn thingy<'a>() -> (Box<i32>, &'a i32) {
let x = box 1701;
(x, &x)
}
"But... I know that the box will continue to live on, why does the compiler say it doesn't?!" Because Rust can't reason about heap lifetimes and must resort to tying the lifetime of &x to the variable x, not the heap allocation it happens to point to.
Is it the case that pointer checking isn't done entirely at compile-time, and Rust's smart pointers still introduce some runtime overhead compared to, say, raw pointers in C?
There are special runtime-checks for things that can't be checked at compile time. These are usually found in the cell crate. But in general, Rust checks everything at compile time and should produce the same code as you would in C (if your C-code isn't doing undefined stuff).
Or is it possible that the Rust compiler can't make fully correct decisions, and it sometimes needs to Just Trust The Programmer™, probably using one of the lifetime annotations (the ones with the <'lifetime_ident> syntax)? In this case, does this mean that the pointer/memory safety guarantee is not 100%, and still relies on the programmer writing correct code?
If the compiler cannot make the correct decision you get a compile time error telling you that the compiler cannot verify what you are doing. This might also restrict you from stuff you know is correct, but the compiler doesn't. You can always go to unsafe code in that case. But as you correctly assumed, then the compiler relies partly on the programmer.
The compiler checks the function's implementation, to see if it does exactly what the lifetimes say it does. Then, at the call-site of the function, it checks if the programmer uses the function correctly. This is similar to type-checking. A C++ compiler checks if you are returning an object of the correct type. Then it checks at the call-site if the returned object is stored in a variable of the correct type. At no time can the programmer of a function break the promise (except if unsafe is used, but you can always let the compiler enforce that no unsafe is used in your project)
Rust is continuously improved. More things may get legal in Rust once the compiler becomes smarter.
Another possibility is that Rust pointers are non-"universal" or restricted in some sense, so that the compiler can infer their properties entirely during compile-time, but they are not as useful as e. g. raw pointers in C or smart pointers in C++.
There's a few things that can go wrong in C:
dangling pointers
double free
null pointers
wild pointers
These don't happen in safe Rust.
You can never have a pointer that points to an object no longer on the stack or heap. That's proven at compile time through lifetimes.
You do not have manual memory management in Rust. Use a Box to allocate your objects (similar but not equal to a unique_ptr in C++)
Again, no manual memory management. Boxes automatically free memory.
In safe Rust you can create a pointer to any location, but you cannot dereference it. Any reference you create always is bound to an object.
There's a few things that can go wrong in C++:
everything that can go wrong in C
SmartPointers only help you not forget calling free. You can still create dangling references: auto x = make_unique<int>(42);
auto& y = *x;
x.reset();
y = 99;
Rust fixes those:
see above
as long as y exists, you may not modify x. This is checked at compile time and cannot be circumvented by more levels of indirection or structs.
I have read somewhere that in a language that features pointers, it is not possible for the compiler to decide fully at compile time whether all pointers are used correctly and/or are valid (refer to an alive object) for various reasons, since that would essentially constitute solving the halting problem.
Rust doesn't prove you all pointers are used correctly. You may still write bogus programs. Rust proves that you are not using invalid pointers. Rust proves that you never have null-pointers. Rust proves that you never have two pointers to the same object, execept if all these pointers are non-mutable (const). Rust does not allow you to write any program (since that would include programs that violate memory safety). Right now Rust still prevents you from writing some useful programs, but there are plans to allow more (legal) programs to be written in safe Rust.
That is not surprising, intuitively, because in this case, we would be able to infer the runtime behavior of a program during compile-time, similarly to what's stated in this related question.
Revisiting the example in your referenced question about the halting problem:
void foo() {
if (bar() == 0) this->a = 1;
}
The above C++ code would look one of two ways in Rust:
fn foo(&mut self) {
if self.bar() == 0 {
self.a = 1;
}
}
fn foo(&mut self) {
if bar() == 0 {
self.a = 1;
}
}
For an arbitrary bar you cannot prove this, because it might access global state. Rust soon gets const functions, which can be used to compute stuff at compile-time (similar to constexpr). If bar is const, it becomes trivial to prove if self.a is set to 1 at compile-time. Other than that, without pure functions or other restrictions of the function content, you can never prove whether self.a is set to 1 or not.
Rust currently doesn't care whether your code is called or not. It cares whether the memory of self.a still exists during the assignment. self.bar() can never destroy self (except in unsafe code). Therefor self.a will always be available inside the if branch.
Most of the safety of Rust references is guaranteed by strict rules:
If you posses a const reference (&), you can clone this reference and pass it around, but not create a mutable &mut reference out of it.
If a mutable (&mut) reference to an object exists, no other reference to this object can exist.
A reference is not allowed to outlive the object it refers to, and all functions manipulating references must declare how the references from their input and output are linked, using lifetime annotations (like 'a).
So in terms of expressiveness, we are effectively more limited than when using plain raw pointers (for example, building a graph structure is not possible using only safe references), but these rules can effectively be completely checked at compile-time.
Yet, it is still possible to use raw pointers, but you have to enclose the code dealing with them in a unsafe { /* ... */ } block, telling to the compiler "Trust me, I know what I am doing here". That is what some special smart pointers do internally, such as RefCell, which allows you to have these rules checked at runtime rather than compile-time, to gain expressiveness.

How are Rust's Arc and Rc types different from having garbage collection?

The Rust Programming Language, first edition says that Rust does not have a garbage collector:
It maintains these goals without having a garbage collector
However, in discussing choosing your guarantees it also says:
Rc<T> is a reference counted pointer. In other words, this lets us have multiple "owning" pointers to the same data, and the data will be dropped (destructors will be run) when all pointers are out of scope.
As far as I understand, this is exactly how pointers work in a garbage-collected language like Python.
I consider garbage collection to be any process which prevents the need for manual deallocation of dynamically-allocated memory. I think I don't understand what the Rust guide considers to be garbage collection however.
I consider garbage collection to be any process which prevents the need for manual deallocation of dynamically-allocated memory
Then Rust does have "garbage collection"!
fn make_stuff() {
// Allocate memory on the heap and store `true` in it
let thing = Box::new(true);
// Allocate memory on the heap and store 1000 numbers in it.
let things = vec![42; 1000];
} // Look Ma! No manual deallocation!
When thing and things go out of scope (in this case, at the end of the method), then the memory they had allocated will be freed for you.
Rc and Arc allow more flexibility than this; you should give their docs a read to know more.
In addition to #Manishearth's answer, there's also this detail (emphasis mine):
automatically freed at the end of its last owner's lifetime
In many garbage-collected languages, garbage collection happens out-of-band with the rest of your code. In Rust, the location of deallocation will be known.
Given this Java:
public static ArrayList alpha()
{
return new ArrayList();
}
public static void beta()
{
alpha(); // Unused result
}
I do not believe that you can say with certainty when the ArrayList will be removed from memory. In the equivalent Rust code, you know that an Arc or Rc will be destructed as soon as it goes out of scope.
This article is a bit old, regarding how Rust has changed now, but it highlights what it means that Rust does not have GC. Only RAII and ownership are intrinsic to Rust. They helps writing reference-counting alike GCs such as Rc and Arc, but those are not part of the language, they're part of the standard library. And it makes a huge difference.
If you consider writing an operating system in Rust, you can't use any form of GC in part of your code or use the standard library. At this level, it's important to know what is part of the language and what is not. For a simple example, have look here.
In contrast, in a language such as Java or Python, you can't prevent your code from using the GC as it use it implicitly by design of the language.
In Rust, like in C/C++ a GC is part from a library and it's use is explicit.
Rc has no cycle collection. If you create a cycle of references, you will probably crash the program as it tries to increment the refcount.
Though this technically counts as a garbage collector too, it's not a universally useful one since you have the restriction on the types it can contain.
If you insist on being correct, a garbage collector collects. It collects a list of memory locations to free. Rc/Arc on the other hand does not, so I don't think you can call it garbage collection.
However rust has the std::gc module, so yea rust has in fact an optional garbage collector for now.

Resources