Blame docs/dev-ast-memory.md

Packit bfcc33
# LibSass smart pointer implementation
Packit bfcc33
Packit bfcc33
LibSass uses smart pointers very similar to `shared_ptr` known
Packit bfcc33
by Boost or C++11. Implementation is a bit less modular since
Packit bfcc33
it was not needed. Various compile time debug options are
Packit bfcc33
available if you need to debug memory life-cycles.
Packit bfcc33
Packit bfcc33
Packit bfcc33
## Memory Classes
Packit bfcc33
Packit bfcc33
### SharedObj
Packit bfcc33
Packit bfcc33
Base class for the actual node implementations. This ensures
Packit bfcc33
that every object has a reference counter and other values.
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
class AST_Node : public SharedObj { ... };
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
### SharedPtr (base class for SharedImpl)
Packit bfcc33
Packit bfcc33
Base class that holds on to the pointer. The reference counter
Packit bfcc33
is stored inside the pointer object directly (`SharedObj`).
Packit bfcc33
Packit bfcc33
### SharedImpl (inherits from SharedPtr)
Packit bfcc33
Packit bfcc33
This is the main base class for objects you use in your code. It
Packit bfcc33
will make sure that the memory it points at will be deleted once
Packit bfcc33
all copies to the same object/memory go out of scope.
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
Class* pointer = new Class(...);
Packit bfcc33
SharedImpl<Class> obj(pointer);
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
To spare the developer of typing the templated class every time,
Packit bfcc33
we created typedefs for each available AST Node specialization.
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
typedef SharedImpl<Number> Number_Obj;
Packit bfcc33
Number_Obj number = SASS_MEMORY_NEW(...);
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
Packit bfcc33
## Memory life-cycles
Packit bfcc33
Packit bfcc33
### Pointer pickups
Packit bfcc33
Packit bfcc33
I often use the terminology of "pickup". This means the moment when
Packit bfcc33
a raw pointer not under any control is assigned to a reference counted
Packit bfcc33
object (`XYZ_Obj = XYZ_Ptr`). From that point on memory will be
Packit bfcc33
automatically released once the object goes out of scope (but only
Packit bfcc33
if the reference counter reaches zero). Main point beeing, you don't
Packit bfcc33
have to worry about memory management yourself.
Packit bfcc33
Packit bfcc33
### Object detach
Packit bfcc33
Packit bfcc33
Sometimes we can't return reference counted objects directly (see
Packit bfcc33
invalid covariant return types problems below). But we often still
Packit bfcc33
need to use reference objects inside a function to avoid leaks when
Packit bfcc33
something throws. For this you can use `detach`, which basically
Packit bfcc33
detaches the pointer memory from the reference counted object. So
Packit bfcc33
when the reference counted object goes out of scope, it will not
Packit bfcc33
free the attached memory. You are now again in charge of freeing
Packit bfcc33
the memory (just assign it to a reference counted object again).
Packit bfcc33
Packit bfcc33
Packit bfcc33
## Circular references
Packit bfcc33
Packit bfcc33
Reference counted memory implementations are prone to circular references.
Packit bfcc33
This can be addressed by using a multi generation garbage collector. But
Packit bfcc33
for our use-case that seems overkill. There is no way so far for users
Packit bfcc33
(sass code) to create circular references. Therefore we can code around
Packit bfcc33
this possible issue. But developers should be aware of this limitation.
Packit bfcc33
Packit bfcc33
There are AFAIR two places where circular references could happen. One is
Packit bfcc33
the `sources` member on every `Selector`. The other one can happen in the
Packit bfcc33
extend code (Node handling). The easy way to avoid this is to only assign
Packit bfcc33
complete object clones to these members. If you know the objects lifetime
Packit bfcc33
is longer than the reference you create, you can also just store the raw
Packit bfcc33
pointer. Once needed this could be solved with weak pointers.
Packit bfcc33
Packit bfcc33
Packit bfcc33
## Addressing the invalid covariant return types problems
Packit bfcc33
Packit bfcc33
If you are not familiar with the mentioned problem, you may want
Packit bfcc33
to read up on covariant return types and virtual functions, i.e.
Packit bfcc33
Packit bfcc33
- http://stackoverflow.com/questions/6924754/return-type-covariance-with-smart-pointers
Packit bfcc33
- http://stackoverflow.com/questions/196733/how-can-i-use-covariant-return-types-with-smart-pointers
Packit bfcc33
- http://stackoverflow.com/questions/2687790/how-to-accomplish-covariant-return-types-when-returning-a-shared-ptr
Packit bfcc33
Packit bfcc33
We hit this issue at least with the CRTP visitor pattern (eval, expand,
Packit bfcc33
listize and so forth). This means we cannot return reference counted
Packit bfcc33
objects directly. We are forced to return raw pointers or we would need
Packit bfcc33
to have a lot of explicit and expensive upcasts by callers/consumers.
Packit bfcc33
Packit bfcc33
### Simple functions that allocate new AST Nodes
Packit bfcc33
Packit bfcc33
In the parser step we often create new objects and can just return a
Packit bfcc33
unique pointer (meaning ownership clearly shifts back to the caller).
Packit bfcc33
The caller/consumer is responsible that the memory is freed.
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
typedef Number* Number_Ptr;
Packit bfcc33
int parse_integer() {
Packit bfcc33
  ... // do the parsing
Packit bfcc33
  return 42;
Packit bfcc33
}
Packit bfcc33
Number_Ptr parse_number() {
Packit bfcc33
  Number_Ptr p_nr = SASS_MEMORY_NEW(...);
Packit bfcc33
  p_nr->value(parse_integer());
Packit bfcc33
  return p_nr;
Packit bfcc33
}
Packit bfcc33
Number_Obj nr = parse_number();
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
The above would be the encouraged pattern for such simple cases.
Packit bfcc33
Packit bfcc33
### Allocate new AST Nodes in functions that can throw
Packit bfcc33
Packit bfcc33
There is a major caveat with the previous example, considering this
Packit bfcc33
more real-life implementation that throws an error. The throw may
Packit bfcc33
happen deep down in another function. Holding raw pointers that
Packit bfcc33
we need to free would leak in this case.
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
int parse_integer() {
Packit bfcc33
  ... // do the parsing
Packit bfcc33
  if (error) throw(error);
Packit bfcc33
  return 42;
Packit bfcc33
}
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
With this `parse_integer` function the previous example would leak memory.
Packit bfcc33
I guess it is pretty obvious, as the allocated memory will not be freed,
Packit bfcc33
as it was never assigned to a SharedObj value. Therefore the above code
Packit bfcc33
would better be written as:
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
typedef Number* Number_Ptr;
Packit bfcc33
int parse_integer() {
Packit bfcc33
  ... // do the parsing
Packit bfcc33
  if (error) throw(error);
Packit bfcc33
  return 42;
Packit bfcc33
}
Packit bfcc33
// this leaks due to pointer return
Packit bfcc33
// should return Number_Obj instead
Packit bfcc33
// though not possible for virtuals!
Packit bfcc33
Number_Ptr parse_number() {
Packit bfcc33
  Number_Obj nr = SASS_MEMORY_NEW(...);
Packit bfcc33
  nr->value(parse_integer()); // throws
Packit bfcc33
  return &nr; // Ptr from Obj
Packit bfcc33
}
Packit bfcc33
Number_Obj nr = parse_number();
Packit bfcc33
// will now be freed automatically
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
The example above unfortunately will not work as is, since we return a
Packit bfcc33
`Number_Ptr` from that function. Therefore the object allocated inside
Packit bfcc33
the function is already gone when it is picked up again by the caller.
Packit bfcc33
The easy fix for the given simplified use case would be to change the
Packit bfcc33
return type of `parse_number` to `Number_Obj`. Indeed we do it exactly
Packit bfcc33
this way in the parser. But as stated above, this will not work for
Packit bfcc33
virtual functions due to invalid covariant return types!
Packit bfcc33
Packit bfcc33
### Return managed objects from virtual functions
Packit bfcc33
Packit bfcc33
The easy fix would be to just create a new copy on the heap and return
Packit bfcc33
that. But this seems like a very inelegant solution to this problem. I
Packit bfcc33
mean why can't we just tell the object to treat it like a newly allocated
Packit bfcc33
object? And indeed we can. I've added a `detach` method that will tell
Packit bfcc33
the object to survive deallocation until the next pickup. This means
Packit bfcc33
that it will leak if it is not picked up by consumer.
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
typedef Number* Number_Ptr;
Packit bfcc33
int parse_integer() {
Packit bfcc33
  ... // do the parsing
Packit bfcc33
  if (error) throw(error);
Packit bfcc33
  return 42;
Packit bfcc33
}
Packit bfcc33
Number_Ptr parse_number() {
Packit bfcc33
  Number_Obj nr = SASS_MEMORY_NEW(...);
Packit bfcc33
  nr->value(parse_integer()); // throws
Packit bfcc33
  return nr.detach();
Packit bfcc33
}
Packit bfcc33
Number_Obj nr = parse_number();
Packit bfcc33
// will now be freed automatically
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
Packit bfcc33
## Compile time debug options
Packit bfcc33
Packit bfcc33
To enable memory debugging you need to define `DEBUG_SHARED_PTR`.
Packit bfcc33
This can i.e. be done in `include/sass/base.h`
Packit bfcc33
Packit bfcc33
```c++
Packit bfcc33
define DEBUG_SHARED_PTR
Packit bfcc33
```
Packit bfcc33
Packit bfcc33
This will print lost memory on exit to stderr. You can also use
Packit bfcc33
`setDbg(true)` on sepecific variables to emit reference counter
Packit bfcc33
increase, decrease and other events.
Packit bfcc33
Packit bfcc33
Packit bfcc33
## Why reinvent the wheel when there is `shared_ptr` from C++11
Packit bfcc33
Packit bfcc33
First, implementing a smart pointer class is not really that hard. It
Packit bfcc33
was indeed also a learning experience for myself. But there are more
Packit bfcc33
profound advantages:
Packit bfcc33
Packit bfcc33
- Better GCC 4.4 compatibility (which most code still has OOTB)
Packit bfcc33
- Not thread safe (give us some free performance on some compiler)
Packit bfcc33
- Beeing able to track memory allocations for debugging purposes
Packit bfcc33
- Adding additional features if needed (as seen in `detach`)
Packit bfcc33
- Optional: optimized weak pointer implementation possible
Packit bfcc33
Packit bfcc33
### Thread Safety
Packit bfcc33
Packit bfcc33
As said above, this is not thread safe currently. But we don't need
Packit bfcc33
this ATM anyway. And I guess we probably never will share AST Nodes
Packit bfcc33
across different threads.