Memory · Lesson 15.15

Rc

Source
Share one value between several owners with Rc<T>, whose copies count themselves: the value is read through Get, written through GetMutable only while one handle holds it, and destroyed when the last handle goes.

A Box<T> knows when to free its value because it is the value's only owner. Some values have no single owner. An editor's colour theme is used by every open window; a loaded image may be shown in three places. The memory should go when the last user is done with it — and no one user knows whether it is the last.

Rc<T> — a reference-counted handle — answers that by counting:

import Allocator::{ AllocError, Allocator, Rc, SystemAllocator };

Creating and copying handles

Rc::Create<T>(allocator, value) moves the value into allocated memory beside a count, which starts at one, and returns the first handle. Like Box::Create, it can fail, so it returns Rc<T> ! AllocError:

var theme = Rc::Create<Theme>(allocator, Theme { name: "paper", size: 11 })?;

Copying a handle does not copy the value. Rc has a custom copy operator that adds one to the count, and the new handle reaches the same value. So every window below holds a handle to the theme, not a theme of its own:

struct Window {
    title: char8[..];
    theme: Rc<Theme>;
}
flowchart LR
    m["`theme` in `Main`"] --> v["the paper theme<br/>count: 4"]
    p["`theme` parameter"] --> v
    e["`editor.theme`"] --> v
    w["`preview.theme`"] --> v

Count() reads the count. Passing a handle by value — or a struct that holds one — copies it too, which is why Draw sees five handles while the call lasts.

The last handle frees the value

When a handle's life ends, the count goes down by one. The handle that takes it to zero destroys the value — running its destructor — and gives the memory back. In the program, every handle made in OpenWindows ends with the call, and the count returns to one:

MomentHandles to the paper theme
after Rc::Create1
inside OpenWindows, two windows made4
inside Draw5
OpenWindows returned1
Main ends0 — ~Theme runs

Nothing in the program says when to free the theme. the paper theme is destroyed is printed after end of Main: because Main's handle was the last.

Reading and writing

Get() returns a *T, a read-only pointer, valid while any handle exists. A shared value is read by everybody, and a change made through one handle would surprise all the others, so there is no way to write through Get().

GetMutable() returns (*var T)? — a writable pointer while this handle is the only one, and none while the value is shared:

match theme.GetMutable() {
    value? => value.size = 12,
    none => PrintLine("shared, so read-only")
}

Right after Create there is one handle, so the program may set the size. Inside OpenWindows the same call answers none. A value that must change while it is shared needs a different design: build a new value and replace the handle, or keep the changing part outside the shared one.

Same value, or equal value

SameAs asks whether two handles reach the same value, not whether two values are equal:

let lookalike = Rc::Create<Theme>(allocator, Theme { name: "lookalike", size: 12 })?;
PrintLine("    editor and preview share: {}", editor.theme.SameAs(preview.theme));
PrintLine("    lookalike is shared:      {}", lookalike.SameAs(theme));

The lookalike is a separate value with its own count of one, so it is destroyed as soon as its only handle goes, at the end of OpenWindows.

One thread only.
An Rc keeps its count with plain arithmetic, which is fast but not safe when two threads change it at once. To share a value between threads, the Allocator package has a twin, Arc<T>, which counts atomically. It comes with Arc in Part 25.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A `Box<T>` has exactly one owner, which is what lets it know when to free its value. Some values
// have no single owner, though: one theme used by every window of an editor, one loaded image
// shown in several places. Whichever user goes last should free it, and none of them knows
// which one that is.
//
// `Rc<T>`, a reference-counted handle, settles it by counting. `Rc::Create` moves the value into
// allocated memory with a count of one. Copying a handle does not copy the value: it adds one
// to the count, and both handles reach the same value. When a handle's life ends the count goes
// down by one, and the handle that takes it to zero destroys the value and frees the memory.
//
// Shared values are read-only. `Get()` returns a `*T` for reading. `GetMutable()` returns a
// `*var T` only while a single handle exists, since a change made through one handle would
// otherwise surprise the others; while the value is shared it returns `none`.
//
// An `Rc` counts with plain arithmetic, so its handles must all stay on one thread. Its twin,
// `Arc`, counts atomically for values shared between threads; it comes with the Threads part.
import Allocator::{ AllocError, Allocator, Rc, SystemAllocator };
import Io::PrintLine;

struct Theme {
    name: char8[..];
    size: int32;
}

extend Theme {
    func ~Theme(self: &var Theme) {
        PrintLine("    the {} theme is destroyed", self.name);
    }
}

// Every window holds a handle to its theme, not a copy of it.
struct Window {
    title: char8[..];
    theme: Rc<Theme>;
}

// The window arrives by value, so for the length of the call there is one more handle.
func Draw(window: Window) {
    let theme = window.theme.Get();
    PrintLine("    {} in {} at {}pt, {} handles", window.title, theme.name, theme.size,
        window.theme.Count());
}

func OpenWindows(theme: Rc<Theme>, allocator: Allocator) -> ! AllocError {
    // Two more handles: the count is now one for `Main`, one for this parameter and one each
    // for the windows.
    let editor = Window { title: "editor", theme: theme };
    let preview = Window { title: "preview", theme: theme };
    PrintLine("    {} handles", theme.Count());
    Draw(editor);

    // `GetMutable` needs a `var` handle, so take one more copy. Even so, the answer is `none`:
    // while the value is shared, nobody may write to it.
    var mine = theme;
    PrintLine("    can write: {}", mine.GetMutable() != none);

    // `SameAs` asks whether two handles reach one value, not whether two values are equal. A
    // second theme with the same fields is still a different value, with its own count.
    let lookalike = Rc::Create<Theme>(allocator, Theme { name: "lookalike", size: 12 })?;
    PrintLine("    editor and preview share: {}", editor.theme.SameAs(preview.theme));
    PrintLine("    lookalike is shared:      {}", lookalike.SameAs(theme));

    // Every handle made here ends with the call. The lookalike was the only handle to its
    // value, so that value is destroyed now; the paper theme still has `Main`'s handle.
}

func Main() -> ! AllocError {
    var system = SystemAllocator();
    let allocator: Allocator = system;

    var theme = Rc::Create<Theme>(allocator, Theme { name: "paper", size: 11 })?;
    PrintLine("created, {} handle", theme.Count());

    // A lone handle may write. `GetMutable` returns `(*var Theme)?`, a pointer or `none`.
    match theme.GetMutable() {
        value? => value.size = 12,
        none => PrintLine("shared, so read-only")
    }

    // `Get()` gives only a `*Theme`, so writing through it is refused:
    //
    //     error: cannot modify data through read-only pointer '*Theme'

    PrintLine("opening windows:");
    OpenWindows(theme, allocator)?;
    PrintLine("windows closed, {} handle", theme.Count());

    // `theme` is the last handle, so the value is destroyed when `Main` ends, and only then.
    PrintLine("end of Main:");
}

Besides Io, its Rux.toml lists Allocator under [Dependencies].

Run it

cd Examples/Memory/Rc
rux run
created, 1 handle
opening windows:
    4 handles
    editor in paper at 12pt, 5 handles
    can write: false
    editor and preview share: true
    lookalike is shared:      false
    the lookalike theme is destroyed
windows closed, 1 handle
end of Main:
    the paper theme is destroyed

Common mistakes

Try it yourself

  1. Print theme.Count() inside Draw before and after let copy = window.theme;. How much does one more let add?
  2. In Main, after OpenWindows, try GetMutable again and set the size to 14. Does it work now, and why?
  3. Make the lookalike a second handle to theme instead of a new value. What does SameAs say, and what happens to the the lookalike theme is destroyed line?
  4. Give Window a destructor that prints its title, and watch the order in which windows and themes go.

Learn more