Resources
Normative rules: spec §6.8 Resources and destruction.
Almost everything in vilan is a value: it copies, and the copy is yours (the memory model chapter). A few things can’t work that way. A database handle that copied would close twice. A task owner that copied would cancel the wrong tasks. These are resources: values with a single owner, that move instead of copying, and that are torn down deterministically when their owner’s scope ends.
You mark one with resource, and — if it needs cleanup — give it a Drop:
import std::print;
import std::drop::Drop;
resource struct Guard {
label: str,
}
impl Guard with Drop {
fun drop(&mut self) {
print(i"dropped {self.label}");
}
}
fun main() {
let first = Guard { label = "first" };
let second = Guard { label = "second" };
print("body");
}
That program prints body, then dropped second, then dropped first.
Two things to notice: drop ran on its own at the end of main, with no
call from you — and the two guards tore down in reverse order, the way a
stack unwinds.
Moving, not copying
Binding a resource to a new name moves it. The old name is now empty, and using it is an error:
let a = Guard { label = "a" };
let b = a; // a moves into b
print(a.label); // ✗ error: use of `a` after it was moved
The compiler points at the use and, in a note, at the move: a resource has
one owner, so once you hand it over the old binding is spent. The same
happens when you pass a resource to an own parameter, return it, or match
it by value.
Loaning instead of moving
Usually you don’t want to give a resource away — you want to use it and
keep it. Lend a loan (a view, & or &mut), exactly as with ordinary
values:
import std::print;
import std::drop::Drop;
resource struct Guard { label: str }
impl Guard with Drop {
fun drop(&mut self) { print(i"dropped {self.label}"); }
}
fun inspect(&g: Guard) {
print(i"inspecting {g.label}");
}
fun main() {
let g = Guard { label = "g" };
inspect(&g);
inspect(&g);
print("done");
}
A loan changes no ownership, so g is still yours after each call — the
program prints inspecting g twice, then done, then dropped g when
main ends. Method calls (&self, &mut self) are loans too; that is how
a resource’s own methods reach it without consuming it.
Teardown happens on every exit
Because teardown is tied to the owner’s scope, it runs however the scope
ends — falling off the bottom, an early ret, a jump out of a loop, even
a panic unwinding through. There are no drop flags and nothing to remember:
if a binding still owns a resource when control leaves the scope, it drops.
Tearing down early: drop(x)
Sometimes you want a resource gone before its scope ends. Move it into
drop:
import std::print;
import std::drop::{ Drop, drop };
resource struct Guard { label: str }
impl Guard with Drop {
fun drop(&mut self) { print(i"dropped {self.label}"); }
}
fun main() {
let a = Guard { label = "a" };
let b = Guard { label = "b" };
drop(a); // a is torn down right here
print("after drop(a)");
}
This prints dropped a, after drop(a), dropped b. drop takes its
argument by move, so a is spent at that line — there is no close() to
call and no way to use a afterward by mistake. (On plain data, drop(x)
just means “I’m done with this”; it does nothing.)
Conditional teardown: Option.take
A resource can live in an Option, which is the one container that holds
one. take() moves the resource out and leaves None behind — exactly what
“tear it down only if it’s there” needs:
import std::print;
import std::drop::{ Drop, drop };
import std::option::Option::{ self, Some, None };
resource struct Guard { label: str }
impl Guard with Drop {
fun drop(&mut self) { print(i"dropped {self.label}"); }
}
fun main() {
mut slot: Option<Guard> = Some(Guard { label = "held" });
match slot.take() {
Some(let g) => drop(g),
None => {},
}
print("after take");
}
After the take, slot is None, so nothing drops a second time at the
end of main. take is also how a resource leaves a struct field — the one
sanctioned way to move a resource out of something that is still alive.
A real resource: Database
The std Database is a resource: opening one gives you a handle that closes
itself on drop. A short-lived database closes when its function returns; a
server that runs forever wants the opposite, so it keeps the database at
module level, where it lives for the whole process and never drops:
import std::print;
import std::db::{ Database, Row };
import std::option::Option::{ self, Some, None };
let db: Database = Database::open(":memory:");
fun setup() {
db.exec("CREATE TABLE items (name TEXT)");
db.prepare("INSERT INTO items (name) VALUES (?)").run(["widget"]);
}
fun count_items(): i32 {
let statement = db.prepare("SELECT COUNT(*) AS n FROM items");
match statement.first([]) {
Some(let row) => row.integer("n"),
None => 0,
}
}
fun main() {
setup();
print(i"items: {count_items()}");
}
Every function reaches db by loan (a method call is a loan), never by
moving it. A module-level resource is loan-only for exactly this reason:
moving or droping it would close the shared handle out from under the rest
of the program, so the compiler rejects that. When you do want a database
that closes at the end of a scope, open it in a local instead — or drop(db)
to close it early.
A closure may reach a module-level resource too. A closure that
references db isn’t capturing an owner — it borrows the same
process-lifetime storage, per call, exactly as a function does. This is what
gives the module-level idiom its reach: request handlers, injected hooks, and
background tasks can all touch the database, as long as it lives at module
level.
import std::db::Database;
import std::shared::Shared;
let db: Database = Database::open(":memory:");
fun main() {
db.exec("CREATE TABLE account (username TEXT)");
// A hook closure that reaches the module-level `db` — not a captured
// owner, just a per-call loan of process-lifetime storage.
let insert = |username: str| {
db.prepare("INSERT INTO account (username) VALUES (?)").run([username]);
};
let hook = Shared::new(insert);
hook.read()("alice");
}
A local resource is different: a closure that captures one would become a second owner, so that stays rejected (below).
Owning background work: OwnedNursery
A closure can’t capture a local resource (it would become a second owner),
which is a problem for background tasks that need to outlive the function
that starts them. OwnedNursery is the answer: a resource that owns the tasks
spawned inside its enter, and cancels them when it drops.
import std::print;
import std::time::sleep;
import std::task::OwnedNursery;
fun main() {
let owner = OwnedNursery::new();
owner.enter(|| {
let _ = async {
sleep(50);
print("background work");
};
});
print("enter returned without joining");
}
Unlike a nursery, enter does not wait for the spawned work — it returns
right away, and the tasks keep running under owner. When owner drops (at
the end of main, or at an explicit drop(owner)), the tasks are cancelled.
That is the whole point: the owner’s lifetime bounds the work, and the
owner is an ordinary resource the scope rules already know how to tear down.
Traps
- “Why can’t I use it again?” You moved it. Loan it (
&x/&mut x, or a method call) instead of binding, passing, or returning it by value. - A resource can’t go in a
List,Map, orSet— the compiler can’t see inside those. Use anOption, or a struct field. - A closure or spawn can’t capture a local resource. Pass a loan into
the call, give the resource to a struct (or an
OwnedNursery) that owns the closure’s lifetime, or keep it at module level — a module global is loan-only and process-lifetime, so a closure may reach it without becoming an owner. Dropis only for resources, must be exactlyfun drop(&mut self), and must be synchronous and context-free (noawait, no signal writes). Cancel owned tasks through anOwnedNurseryrather than awaiting them.