RAII and Drop
Deterministic resource cleanup with drop, defer, with, and manual destruction.
RAII in Zynx means that an owning value is responsible for releasing the resource it represents. The compiler tracks moves, inserts deterministic cleanup for owners, and rejects use after ownership has been transferred.
Use Move, Copy, and Clone for the ownership introduction. This page focuses on cleanup behavior.
A struct destructor is declared with the special drop(self) member. It has no
fn keyword and no return type.
import std.os;
struct Tracer {
id: i32,
drop(self) {
_ = os.write("drop {self.id}\n");
}
}
fn main() {
let first = Tracer { id: 1 };
let second = Tracer { id: 2 };
_ = os.write("body\n");
}
When the function exits, the two owners are cleaned up in reverse order:
second, then first.
The destructor form is drop(self) { ... }. A normal method named
fn drop(self) is just a method; it is not the language destructor that
automatic cleanup calls.
An owner is dropped when its lifetime ends. In everyday code that usually means
the end of the scope, including early exits through return, break, and
continue.
import std.os;
struct Guard {
label: str,
drop(self) {
_ = os.write("leave {self.label}\n");
}
}
fn open() -> i32 {
let guard = Guard { label: "open" };
_ = guard.label;
return 1;
}
Cleanup is deterministic and path-sensitive, not garbage-collected. The current implementation uses liveness analysis to place cleanup where an owner is no longer live, so do not rely on a destructor being delayed until a closing brace when the value is already dead.
Moving a resource transfers the eventual cleanup to the new owner. The moved-from binding is no longer usable and is not dropped separately.
import std.os;
struct Handle {
id: i32,
drop(self) {
_ = os.write("close {self.id}\n");
}
}
fn main() {
let first = Handle { id: 7 };
let second = first;
_ = os.write("using {second.id}\n");
}
Only second is dropped. Reading first.id after the assignment is rejected as
use after move.
Copy values do not participate in ownership transfer. A struct that declares
drop cannot implement Copy, and every field of a Copy struct must also be
copyable.
Reassigning an owning binding drops the old value before the new value is stored.
import std.os;
struct Tracer {
id: i32,
drop(self) {
_ = os.write("drop {self.id}\n");
}
}
fn main() {
let value = Tracer { id: 1 };
value = Tracer { id: 2 };
_ = os.write("end\n");
}
The previous value is dropped during the reassignment. The replacement is
dropped when its lifetime ends.
Nullable owners only drop a payload when the value is not null. Assigning
null to a nullable owner drops the previous payload first.
When a struct owns fields that also need cleanup, the outer destructor body runs first. Owned fields are cleaned up after that body returns.
import std.os;
struct Inner {
id: i32,
drop(self) {
_ = os.write("drop inner {self.id}\n");
}
}
struct Outer {
inner: Inner,
drop(self) {
_ = os.write("drop outer {self.inner.id}\n");
}
}
fn main() {
let value = Outer { inner: Inner { id: 1 } };
std.drop(value);
}
This prints the outer message before the inner message.
Use std.drop(value) when a resource must be destroyed before its natural cleanup
point.
import std.os;
struct Temp {
id: i32,
drop(self) {
_ = os.write("drop temp {self.id}\n");
}
}
fn main() {
let temp = Temp { id: 1 };
std.drop(temp);
}
std.drop takes one local variable name and is a statement. After
std.drop(temp), temp has been moved and cannot be used again. The
lower-level @drop(temp) spelling is legacy/internal; public code should use
std.drop(temp).
defer is for ad hoc scope cleanup. Deferred actions run in last-in,
first-out order when the current scope exits.
import std.os;
fn main() {
defer _ = os.write("last\n");
defer _ = os.write("first\n");
_ = os.write("body\n");
}
For a scope that has both defer statements and live owners, registered
defers run before automatic owner cleanup for that scope. This lets deferred
code inspect locals that are still live.
Use drop(self) for type-owned resource invariants. Use defer for one-off
actions such as temporary allocation cleanup.
return, break, and continue are not allowed inside defer. In async fn,
await is not allowed inside defer, and a live defer may not cross
await or select.
with expr as name { ... } creates a scoped resource binding and cleans it up
when the block exits.
import std.os;
struct Resource {
id: i32,
drop(self) {
_ = os.write("drop {self.id}\n");
}
}
fn make_resource(id: i32) -> Resource {
return Resource { id };
}
fn main() {
with make_resource(1) as resource {
_ = os.write("inside {resource.id}\n");
}
_ = os.write("after\n");
}
with is the public shape for scoped APIs such as future.Group<T, E>. The binding
does not escape the block.
Raw byte operations do not run destructors. Use std.mem slot helpers only
when you are managing initialized storage yourself.
import std.mem;
fn replace<T>(dst: *T, src: *T) {
mem.drop_at<T>(dst);
mem.move_at<T>(dst, src);
}
read_at, move_at, and drop_at move values out of typed storage. After
using them, treat the source slot as uninitialized unless you write a new value
into it.
Language exits such as return, break, continue, throw, and failed
try unwind source scopes and run cleanup. Runtime traps do not. Bounds
failures, checked arithmetic overflow, invalid casts, null dereference,
assertion failures, and explicit trap builtins abort immediately without
running defer, automatic drops, with cleanup, or async frame cleanup.
Async cancellation is cooperative task completion, not source-level unwinding.
A defer may not live across await, so it is not a cancellation hook. Owned
future frames are still destroyed exactly once by their owning runtime.
- Put resource release in
drop(self), not in a normalfn dropmethod. - Keep destructors local and non-fallible; handle or discard cleanup failures inside the destructor.
- Do not make resource owners
Copy. - Use
deferfor local cleanup actions andwithfor APIs that require a scoped owner. - Use
std.dropsparingly, because it consumes the binding immediately. - Prefer references for checked borrowing and raw pointers only for interop or manual memory.