Hello Everyone,
I’m new to lower‑level programming languages, and Zig has been a great introduction. I’ve mostly worked with Python, where memory management is handled for you, so manually managing memory in Zig especially understanding when and how to free it has been challenging for me.
I’ve built a WIP GapBuffer implementation. The snippet below shows a simple insert operation that adds a single character. Internally, it may grow the buffer by allocating new memory and copying the existing data. Another helper, pushRightDataToEnd, rearranges the buffer by copying part of the data to the end of the current allocation
I’m unsure about the correct points to free allocated memory, which is causing both segfaults and leaks. I’ve added comments in the code to show all the places where I attempted to free memory.
Also, I have a pattern in insert method where I have function signature like (self: *GapBuffer, allocator: Allocator, data: []u8, char: u8), where I am passing the struct pointer, allocator, existing data and the character to be inserted. data argument is constant therefore, assign it to a new mutable variable new_data to let it mutate. After all the modifications we return the new_data back to the caller, is this a good pattern?
//! an example to display memory management issue (seg-fault for now)
//! Note, it is not the exact logic to implement gap buffer
//! this is truncated and modified to display of the said issue.
//! I am also not formatting the code to shorten the example by lines.
const std = @import("std");
const Allocator = std.mem.Allocator;
const print = std.debug.print;
/// GapBuffer struct
const GapBuffer = struct {
gap_start: usize = 0,
cursor_pos: usize = 0,
gap_end: usize,
capacity: usize,
/// init method
pub fn init(capacity: usize) !GapBuffer {
return .{ .gap_end = capacity - 1, .capacity = capacity };
}
/// grow method when a buffer is full
fn grow(self: *GapBuffer, allocator: Allocator, data: []u8) ![]u8 {
// allolcate a new memory space
const new_data = try allocator.alloc(u8, data.len + self.capacity);
const right_data_start = self.gap_end + 1;
const right_data_end = data.len - 1;
var right_data: []u8 = &.{};
if (right_data_start <= right_data_end) { right_data = data[right_data_start .. right_data_end + 1]; }
// memcpy right data of the gap ends to the end of the new_data []u8.
@memcpy(new_data[new_data.len - right_data.len ..], right_data);
self.gap_end += self.capacity;
// return new_data
return new_data;
}
/// insert a character to the buffer
pub fn insert(self: *GapBuffer, allocator: Allocator, data: []u8, char: u8) ![]u8 {
// QUESTION: is it a good pattern?
// data variable being a constant, use a new variable (new_data)
// and finally return new data.
// data being a constant, use a new variable (new_data)
// so that we can mutate it later
var new_data = data;
// if the buffer is full then, free the buffer
if (self.gap_end == self.gap_start) {
// grow the buffer
new_data = try self.grow(allocator, data);
// QUESTION: free the buffer? may be not sure
// allocator.free(new_data);
}
// if user wants to insert data at the cursor location
if (self.cursor_pos != self.gap_start) {
// push the right side data of the cursor to the end of the buffer
new_data = try self.pushRightDataToEnd(allocator, new_data);
// QUESTION: free the buffer? may be not sure
// allocator.free(new_data);
}
// finally, assign the character at the gap start
new_data[self.gap_start] = char;
self.gap_start += 1;
self.cursor_pos = self.gap_start;
// return the new buffer data
return new_data;
}
/// push the right side data of the cursor to the end of the buffer
fn pushRightDataToEnd(self: *GapBuffer, allocator: Allocator, data: []u8) ![]u8 {
const new_data = try allocator.alloc(u8, data.len);
const left_data: []u8 = data[0..self.cursor_pos];
@memcpy(new_data[0..self.cursor_pos], left_data);
const right_data_start = self.cursor_pos;
const right_data_end = self.gap_start - 1;
var right_data: []u8 = &.{};
if (right_data_start <= right_data_end) { right_data = data[right_data_start .. right_data_end + 1]; }
// copy right of the gap
const new_right_data_start = data.len - right_data.len;
@memcpy(new_data[new_right_data_start..], right_data);
self.gap_start = self.cursor_pos;
self.gap_end = new_right_data_start - 1;
return new_data;
}
};
test "test left cursor movement" {
const capacity: u8 = 3;
var gap_buffer = try GapBuffer.init(capacity);
const allocator = std.testing.allocator;
const data = try allocator.alloc(u8, capacity);
// QUESTION: May be free buffer here
defer allocator.free(data);
const insert_times: u8 = 3;
// QUESTION: I also do not like the idea of using a new variable(new_data)
// can we grow and shrink the original(data) buffer and insert the characters to the original buffer?
// First set of inserts
var new_data: []u8 = undefined;
for (0..insert_times) |i| { new_data = try gap_buffer.insert(allocator, data, @intCast(97 + i)); }
// QUESTION: May be free buffer here
allocator.free(new_data);
// second set of inserts
for (0..insert_times) |i| { new_data = try gap_buffer.insert(allocator, new_data, @intCast(97 + i)); }
// QUESTION: May be free buffer here
allocator.free(new_data);
}
Any help will be really appreciated.
Cheers.