Files
bun.sh/src/memory.zig
taylor.fish 73feb108d9 Handle optionals in bun.memory.deinit (#23027)
Currently, if you try to deinit an optional, `bun.memory.deinit` will
silently do nothing, even if the optional's payload is a struct with a
`deinit` method.

This commit makes sure the payload is deinitialized.

(For internal tracking: fixes STAB-1293)
2025-09-26 23:02:44 -07:00

162 lines
5.1 KiB
Zig

//! Basic utilities for working with memory and objects.
/// Allocates memory for a value of type `T` using the provided allocator, and initializes the
/// memory with `value`.
///
/// If `allocator` is `bun.default_allocator`, this will internally use `bun.tryNew` to benefit from
/// the added assertions.
pub fn create(comptime T: type, allocator: std.mem.Allocator, value: T) bun.OOM!*T {
if ((comptime Environment.allow_assert) and isDefault(allocator)) {
return bun.tryNew(T, value);
}
const ptr = try allocator.create(T);
ptr.* = value;
return ptr;
}
/// Frees memory previously allocated by `create`.
///
/// The memory must have been allocated by the `create` function in this namespace, not
/// directly by `allocator.create`.
pub fn destroy(allocator: std.mem.Allocator, ptr: anytype) void {
if ((comptime Environment.allow_assert) and isDefault(allocator)) {
bun.destroy(ptr);
} else {
allocator.destroy(ptr);
}
}
/// Default-initializes a value of type `T`.
///
/// This method tries the following, in order:
///
/// * `.initDefault()`, if a method with that name exists
/// * `.init()`, if a method with that name exists
/// * `.{}`, otherwise
pub fn initDefault(comptime T: type) T {
return if (comptime std.meta.hasFn(T, "initDefault"))
.initDefault()
else if (comptime std.meta.hasFn(T, "init"))
.init()
else
.{};
}
/// Returns true if `T` should not be required to have a `deinit` method.
///
/// This method is primarily for external types where a `deinit` method can't be added.
/// For other types, prefer adding a `deinit` method or adding `pub const deinit = void;` if
/// possible.
fn exemptedFromDeinit(comptime T: type) bool {
return switch (T) {
std.mem.Allocator => true,
else => {
_ = T.deinit; // no deinit method? add one, set to void, or add an exemption
return false;
},
};
}
fn deinitIsVoid(comptime T: type) bool {
return switch (@TypeOf(T.deinit)) {
type => T.deinit == void,
void => true,
else => false,
};
}
/// Calls `deinit` on `ptr_or_slice`, or on every element of `ptr_or_slice`, if the pointer points
/// to a struct or tagged union.
///
/// This function first does the following:
///
/// * If `ptr_or_slice` is a single-item pointer of type `*T`:
/// - If `T` is a struct or tagged union, calls `ptr_or_slice.deinit()`
/// - If `T` is an optional, checks if `ptr_or_slice` points to a non-null value, and if so,
/// calls `bun.memory.deinit` with a pointer to the payload.
/// * If `ptr_or_slice` is a slice, for each element of the slice, calls `bun.memory.deinit` with
/// a pointer to the element.
///
/// Then, if `ptr_or_slice` is non-const, this function also sets all memory referenced by the
/// pointer to `undefined`.
///
/// This method does not free `ptr_or_slice` itself.
pub fn deinit(ptr_or_slice: anytype) void {
const ptr_info = @typeInfo(@TypeOf(ptr_or_slice));
switch (comptime ptr_info.pointer.size) {
.slice => {
for (ptr_or_slice) |*elem| {
deinit(elem);
}
return;
},
.one => {},
else => @compileError("unsupported pointer type"),
}
const Child = ptr_info.pointer.child;
const mutable = !ptr_info.pointer.is_const;
defer {
if (comptime mutable) {
ptr_or_slice.* = undefined;
}
}
const needs_deinit = comptime switch (@typeInfo(Child)) {
.@"struct" => true,
.@"union" => |u| u.tag_type != null,
.optional => {
if (ptr_or_slice.*) |*payload| {
deinit(payload);
}
return;
},
else => false,
};
const should_call_deinit = comptime needs_deinit and
!exemptedFromDeinit(Child) and
!deinitIsVoid(Child);
if (comptime should_call_deinit) {
ptr_or_slice.deinit();
}
}
/// Rebase a slice from one memory buffer to another buffer.
///
/// Given a slice which points into a memory buffer with base `old_base`, return
/// a slice which points to the same offset in a new memory buffer with base
/// `new_base`, preserving the length of the slice.
///
///
/// ```
/// const old_base = [6]u8{};
/// assert(@ptrToInt(&old_base) == 0x32);
///
/// 0x32 0x33 0x34 0x35 0x36 0x37
/// old_base |????|????|????|????|????|????|
/// ^
/// |<-- slice --->|
///
/// const new_base = [6]u8{};
/// assert(@ptrToInt(&new_base) == 0x74);
/// const output = rebaseSlice(slice, old_base, new_base)
///
/// 0x74 0x75 0x76 0x77 0x78 0x79
/// new_base |????|????|????|????|????|????|
/// ^
/// |<-- output -->|
/// ```
pub fn rebaseSlice(slice: []const u8, old_base: [*]const u8, new_base: [*]const u8) []const u8 {
const offset = @intFromPtr(slice.ptr) - @intFromPtr(old_base);
return new_base[offset..][0..slice.len];
}
const std = @import("std");
const Allocator = std.mem.Allocator;
const bun = @import("bun");
const Environment = bun.Environment;
const isDefault = bun.allocators.isDefault;