Owning References
On this page
- Creating Owning References
- Allocating in a Region Value
- Allocating a Finished Value
- Allocating with Contents
- Allocating a Dynamically Sized Body
- C Strings: cstr
- The Methods of str
- Owning Reference Types
- The Order of an Allocation
- Creating Multiple References to the Same Value
- How References and Allocations Expire
- Memory Allocation Errors
- Using a Reference
Note: Contents after a trynew of anything but an array are not implemented.
When you allocate a new value within a region, you get back an owning reference that points to the new object. For some regions, multiple alias copies may be created which are also owning references. For safety reasons, a region's automatic memory management strategy ensures that the object's memory is freed only when when the last owning reference to the object expires. This timing ensures that an owning reference is always safe to de-reference and that an object will only be freed once.
This page describes how owning references are created and used.
Creating Owning References#
Let's ask a region to allocate a new value and give us back an owning reference:
imm node = new Rc[mut, Node](1);
Like every construction, an allocation begins with new. What follows it is the type of the owning reference we want back, specifying:
- Region which allocates the value and manages its lifetime. In this case, the value is allocated in the Rc region. Later, we will explore some of the different kinds of regions supported by Cone. The region may be Rc or So from core, the sync package's Arc (Rc whose count several threads may change at once), the collector package's garbage-collected Gc (custom allocators; discouraged, single threaded and outside actors only), the rcweak package's Rcw, or any struct declaring is RegionRef. The region's own methods, and whether it declares Move, decide when a value is freed; its finalizer runs just before (finalizers).
- Permission. This governs what you may do with the reference. It can be a static or lock permission. uni is assumed for an owning reference when no permission is specified.
- Value type, the type of the value allocated: in this case, a Node.
The parentheses hold the arguments of the value's initializer, chosen as they are for any construction: here, Node's implicit initializer, which takes its fields in order. Empty parentheses may be left off. What we get back is an owning reference of the type written, Rc[mut, Node], which this example stores in the variable node.
Allocating in a Region Value#
A construction whose head is a region value rather than a type, new a[Node](1), allocates in that value's region: an arena's, or, for new self[Node](1) in an actor, the actor's own garbage-collected ActorGc. The compiler reads new a[Node](1) as a.alloc(new Node(1)), so what comes back is what that call returns (an arena returns a borrowed &mut reference to the value). Region values says what may head such a construction; Allocating in an arena and ActorGc show it at work.
Allocating a Finished Value#
The parentheses may instead hold one finished value of the value type, which is moved into the new allocation, as though every type had an initializer taking a finished value of itself. A value no initializer constructs — a number, an array, a tuple, an enum or its variant, a reference — is always allocated this way, and its allocation takes exactly one value, converted to the value type as a variable's initial value is: a number literal takes on the value type, and an enum's variant becomes its enum.
imm count = new Rc[u64](5); // the literal is a u64
imm grid = new Rc[mut, Array[i32, 3]]([1, 2, 3]);
imm pair = new So[(i32, f32)]((1, 2.0));
imm maybe = new So[Option[i32]](Some(3));
imm name = new Arc[imm, String](String.from("x")); // a call's result
imm list = new Rc[mut, List[Fin]](items); // items is moved in
A struct's allocation is told apart by type: when exactly one argument is given and its type is the struct itself (through an alias as well), it is the finished value, moved in, and no initializer runs; otherwise the arguments go to the struct's initializers, as for any construction. A reference to the struct, or a value that would only convert to it, is not the struct itself.
imm s = new Seeded(4); // Seeded's init runs imm byInit = new Rc[Seeded](5); // Seeded's init runs again, on 5 imm moved = new Rc[Seeded](s); // s is moved in; no init runs
A value is constructed once: new Point(p), where p is already a Point, is an error, since a finished value needs no construction; use p as it is.
An alias of an owning reference type allocates the same way, so a program that names its reference type once writes its allocations with that name:
alias Node = Gc[mut, NodeValue]; imm leaf = new Node(1); // a Gc[mut, NodeValue]
To make a virtual reference, allocate the concrete type and let the reference coerce where the virtual one is wanted: imm shape Rc[mut, Shape] = new Rc[mut, Circle](5);
Allocating with Contents#
An allocation takes contents after <-, as a value's construction does (the append operator). An array's contents fill it in the region's memory, an element at a time, each value evaluated as its element is filled: no array is made elsewhere and copied in, so an array of a million elements is allocated and filled with no copy of it anywhere. They are the same contents a value construction of the array takes, the scalars of one written with several sizes, and the allocation takes nothing in parentheses beside them. A collection's contents are appended through the new reference once it is made, as they are appended to a value it constructs.
imm zeros = new Rc[mut, Array[i32, 1000, 1000]] <- fill 0; imm grid = new So[Array[i32, 2, 3]] <- 1, 2, 3, 4, 5, 6; imm names = new Rc[mut, List[i32]](10) <- 1, 2, 3;
Allocating a Dynamically Sized Body#
Some types have no size to hold: a value of one is never in a variable, a field or a parameter, only behind a reference, which carries what is missing. A trait's virtual reference carries a vtable. The body of bytes, str, is reached by a reference that carries their count: a pointer and a usize, in every region and for a borrow alike (&str, So[str], Rc[str], Arc[str], Gc[str]), 16 bytes where a reference to a type with a size is 8. str declares Immutable, so a reference to it written with no permission is imm (&str is &imm str, Rc[str] is Rc[imm, str]) and mut is refused on it. The region's header sits before the first byte as it does for any value, and the owner points at the byte, so one hop reaches the text.
There is no value of such a body to construct or to move in, so its allocation takes a borrow of one in the parentheses and copies the bytes the borrow holds, as many as it has, into the memory the region gives: the region is asked for the header and those bytes. A borrow of the body is made from a slice of bytes with as, and converts to a slice implicitly; both are the same pair and as checks only that, so the way back is the written as, since bytes need not be UTF-8.
imm bytes &Array[u8] = "hello"; imm view = bytes as &str; // a borrow of the bytes: 5 of them imm text = new Rc[str](view); // a copy, in Rc's memory imm same = trynew So[str](view); // an Option, None if memory ran out printStr(text); // lent as a borrow, which is the &str printStr takes
A string literal is a &imm str: a borrow of its text, in read-only memory, that lasts as long as the program, so it may be sent to another thread. The text is followed by a zero byte for C that the count leaves out, and an owner of str keeps one after its bytes too (below). Wherever an owner of str is wanted, in any region, a literal (and only a literal) is copied into one without new; any other &str is copied with new as above.
imm name So[str] = "Padma"; // new So[str]("Padma"), copied from read-only memory
fn greet(who Rc[str]) {..}
greet("Jayne"); // the same copy, into an Rc
A growable String is a literal's other destination: where one is wanted a literal is copied into it (imm s String = "core";), by the static function fromLiteral a type declares to say so. And s.freeze() moves a String's bytes into a So[str] without copying them (collection types).
Where a read-only borrow of such a type is wanted, &String for one, the literal is borrowed from a temporary made the same way: &String.fromLiteral("core"), written out. The temporary lasts to the end of the statement and is dropped there, like any temporary a borrow points at (a function's parameter and an init's, in new File("x"), are given one alike); a local's initializer keeps it to the end of the block, as imm r = &make() does. It is lent only to a borrow that cannot write (&mut String is refused, since what is written to the temporary is lost with it), and only inside a function's body, where a statement ends it: a global, a constant, a field's default, a returned value or an if arm is not given one. A parameter's default is made at each call that leaves the argument out, as if the caller had written the literal there: fn shout(s &String = "core") called as shout() lends a temporary of its own (and a String by value is made for the call).
fn shout(s &String) {..}
shout("core"); // a temporary String, lent to the call, dropped at the semicolon
imm r &String = "core"; // the temporary lasts as long as the block
The other dynamically sized body is Array[T], the array type without a size, whose elements are any type T and whose count is known only at run time. It is held the same way: So[Array[T]], Rc[Array[T]], Arc[Array[T]] and Gc[Array[T]] are each a pointer and a count, one hop to the elements, and its borrow is the slice, &Array[T] (owned arrays). Its allocation copies the elements of a slice, so it is allowed only where the elements copy, and a List's freeze() moves its block into one instead. The owner's death finalizes each element, and a Gc object counts the bytes its elements really take.
imm primes = new Array[i32, 4] <- 2, 3, 5, 7; imm owned = new Rc[imm, Array[i32]](&primes); // a copy: a pointer and a count, 16 bytes imm kept = list.freeze(); // a So[Array[T]] holding a list's elements, moved
C Strings: cstr#
A C function that takes text takes a const char *: one pointer to bytes that end in a zero, whose length it finds by scanning for the zero. Cone's name for that is cstr, a borrowed C string, and a C binding declares its strings with it: extern fn puts(s cstr) i32;. It is a pointer and no more (a one-field struct that crosses to C as one), and it promises the zero. Nothing else does: a &str may be part of a longer text and a slice never has one, so neither converts to a cstr, nor does a raw pointer. What does:
- A literal, wherever a cstr is wanted: puts("hi"). A literal with a zero inside ("ab\x00cd") is refused there, since C would read only ab; the same text is fine as a &str, a String or an owner.
- A String, s.cstr(), with no copy: its block always keeps a zero after its bytes, rewritten by each change, and a String that holds nothing has a static empty literal for one. A zero inside the text would end the string early for C, so cstr() scans for one (at memchr's speed) and panics, at the caller's line, if it finds it. It is good until the String next changes or dies.
- An owner of text keeps the zero after its bytes too, however it was made (copied from a literal or a view, or by s.freeze()): owner.cstr() on a So[str] is its C string, with no copy, checking the text for a zero inside as a String's cstr() does (cstr.fromOwned(&owner) is the same call). A plain &str has no cstr(): it promises no zero.
- A general &str, which promises nothing after its bytes, reaches C by a copy: String.from(s).cstr(). There is no owned C-string type; a String is one.
- A raw pointer the caller knows ends in a zero, cstr.fromPtr(p): an argv entry, what getenv answers when it is not null. A C function that may take or answer a null string stays a *u8 in its binding.
From C, c.view() is the &str of a cstr, found by scanning for the zero and not checked to be UTF-8 (text is UTF-8 by convention), c.len() the count of bytes before it, and c.ptr() the pointer, for a function that takes a buffer. Copy the view into a String to keep it (String.fromCStr(c)); whether C or Cone frees what a C function answered is the binding's knowledge, which copies and then calls C's free.
imm name String = "core";
libc.remove(name.cstr()); // a pointer to its bytes and the zero, no copy
libc.remove("old.tmp"); // a literal is a cstr where one is wanted
imm owner So[str] = "scratch.tmp";
libc.remove(owner.cstr()); // an owner of text is one too, no copy
imm text = libc.strerror(2i32).view(); // a &str of C's answer, scanned
libc.fwrite(bytes as *u8, 1usize, bytes.len, f); // a buffer and a count: a pointer, asked for
A slice is an address and a count, so it is not a pointer: the implicit conversion of a slice to a raw pointer is gone, and xs as *T takes the address of a slice's first element, for a C function that takes a buffer and a length (a slice coerces to no pointer). A buffer and its length are two parameters in C, so they stay a *u8 and a count.
The Methods of str#
str has the methods one expects of text, written once on str itself; a literal, a &str, a So[str] or Rc[str], and any type that lends one, a String, reaches them all. Every position is a number of bytes, so they cost nothing to find; a text is UTF-8 by convention, and where a position must fall between characters the method says so.
- len (bytes, not characters), isEmpty, bytes() (the &Array[u8] of the text), s[i] (the byte, a u8; an index past the end is a program error as for a slice) and isCharBoundary(i) (a character starts at i, or the text ends there).
- A part of the text: &s[a..<b], &s[a..] and &s[a..b] (the last byte included), each a &str borrowed from s, as a slice's range is a slice; they are the calls slice(a, b), sliceFrom(a) and sliceThrough(a, b). A bound that is out of order, past the end, or inside a character is a program error (a panic at the line of the slice), as in Rust: a &str is always whole characters. The range makes a part only when borrowed; the text itself has no size to hold.
- Code points: chars(), charIndices() and charCount(). chars() is a cursor whose next() gives each code point in turn as a char, an Option[char], None past the last; charIndices() gives each with the byte index it starts at, a StrCharIndex with the fields offset and ch (what a lexer needs, and every offset is a place a slice may begin). charCount() counts them by walking the text, O(n), where len is the bytes. A byte that is not part of well-formed UTF-8 reads as U+FFFD, one for each such byte, so every char given is valid. These are code points and not graphemes: a letter and its combining accent are two. The bytes themselves are bytes(), above.
- startsWith(p), endsWith(p), find(p) (an Option of the byte index where p first starts, 0 for an empty p), findFrom(p, i) (at or after i) and contains(p).
- trim(), trimStart() and trimEnd(): a &str without the ASCII whitespace (space, tab, line feed, form feed, carriage return) at the ends.
- ==, !=, <, <=, >, >= and compare(o) (-1, 0 or 1): the bytes compared, and UTF-8's byte order is the order of the code points. Between &str, So[str], Rc[str], a literal and a String, in any pair (Rust's cross-type PartialEq). === asks whether two references are the same place.
- hash(h): feeds a hasher the bytes, then the terminator (h.writeStr), as a String does, so any kind of text hashes as the others of the same bytes do. A Dict[String, V] is asked by a &str for that reason.
An owner of a body is a counted owner or a single one by its region and permission, like any other: copies, parameters and fields are counted (an owner of str, written with no permission, is imm and copies, though an So owner still moves), a uni owner moves, and the last to go gives the block back through the region's free. A collector's region marks it through its pointer. Each owner carries its own count, which is right because the body never grows.
Owning Reference Types#
Wherever a type is written — a variable, a field, a parameter, a return type, an alias, a generic's argument, an allocation — an owning reference type is written the same way: the region, then in square brackets an optional permission and the value type.
alias Link = Rc[mut, Node]; // shared and mutable
fn first(n Rc[Node]) i64 {..} // no permission written: uni, the only owner
mut list = List[Gc[imm, Leaf]].empty();
Left out, the permission is uni, which may become any other, so Rc[Node] is Rc[uni, Node]. The region is a struct declaring is RegionRef; a permission given to anything else is an error. A generic region takes its own brackets first, then the reference's: R[A][mut, Node], and allocates as any other, new R[A][mut, Node](1). When the value type is an open trait, the reference is a virtual reference, which carries a vtable beside its pointer (Rc[mut, Shape]); when it is a body whose length the reference carries, it is a pointer and a count (So[str]); for any other value type, an enum included, it is a single pointer.
A match pattern never spells an owning reference type: it names the variant, Circle, and the matched value supplies the region and the permission, so on an Rc[mut, Shape] it binds an Rc[mut, Circle]. The older plus-sign spelling, +Rc-mut Circle, is refused there too.
The Order of an Allocation#
Every allocation runs its steps in the same order. First the initializer's arguments are evaluated, in the order written, or the finished value is. Then the region obtains the memory, and nothing more (its alloc). Then the region's initializer fills its bookkeeping in place, then a lock permission's does the same for its part, and then the value's initializer fills the value in place, where it was allocated, or the finished value is stored there, or an array's contents fill it, each value evaluated then. Last, the owning reference goes where the expression puts it.
An initializer, or a value of an array's contents, may itself allocate, even in a garbage-collected region, whose collector may run while it does: such a region's new object is held as a root and its value reads as empty from the moment its memory is obtained, so a collection finds it, and nothing it half holds, until it is filled.
imm p = new Rc[mut, Pair](make(1), make(2)); // make(1), make(2), then alloc, then Pair's init imm q = new Rc[mut, Pair](makePair()); // makePair(), then alloc, then the store
Creating Multiple References to the Same Value#
The creation of the first owning reference to a new value always comes from an allocation, as shown above. Additional owning references to that value may then be created by copying an existing reference via assignment or function calls:
imm node = new Rc[imm, Node](4); imm node2 = node; // node2 and node point to same value
How References and Allocations Expire#
Owning references keep the value they point to alive. So long as a value has at least one owning reference that points to it, the value's region will not dispose of the value and reclaim its memory space. Only when all references to a value expire does the region have the right to free the value (although how quickly this happens varies by region).
So, when does the lifetime of an owning reference expire? When the data structure it is bound to expires or changes its value. If the reference is never stored anywhere, it is consumed by the expression it is part of and expires nearly immediately. If the reference is stored in a variable, it expires no later than the end of the lexical scope that variable is declared within (and sooner, obviously, if the variable takes on a new value). If the reference is pointed at by another reference, then the lifetime of the former depends on the lifetime of the latter.
When all references to an allocated value expire and the region has decided to reclaim the value's space, the region will automatically invoke the value's finalizer, if it has been defined.
Memory Allocation Errors#
Although unlikely, it is still possible for memory allocation requests to fail. This typically happens when not enough contiguous, free memory exists to satisfy the request.
Failure to allocate memory triggers a different response depending on how the allocation is written.
- new: the program will panic and shut down, naming the line of the allocation and the size it asked for: out of memory allocating 24 bytes.
- trynew: the allocation gives back an Option of the owning reference, Some holding it when the memory was obtained and None when it was not. Any of the mechanisms provided for handling nullable values may be applied in an attempt to recover from this problem. Only an allocation may fail this way, so trynew takes an owning reference type, never a value type. An array's contents after it are filled only when the memory was obtained: on None, none of their values is evaluated. Contents after a trynew of anything else, such as a list, are refused for now: allocate it with trynew alone, then append on Some, as below.
imm maybe = trynew Rc[mut, Node](1); // an Option[Rc[mut, Node]]
match maybe {
case imm n Some[Rc[mut, Node]] { use(n); }
else { recover(); }
}
imm names = trynew Rc[mut, List[i32]](10); // not: trynew ... <- 1, 2, 3
match names {
case imm s Some[Rc[mut, List[i32]]] { s <- 1, 2, 3; }
else { recover(); }
}
Using a Reference#
By and large, region-managed references may be used in all the same ways as borrowed references. They may be dereferenced, compared, and handled in the same way as owning references.