Defer and function return

My goal is to put the functionality from

const std = @import("std");

pub fn main() !void {
    var debug_allocator = std.heap.DebugAllocator(.{}){};
    defer std.debug.assert(debug_allocator.deinit() == .ok);
    const allocator = debug_allocator.allocator();

    const lang_var = std.process.getEnvVarOwned(allocator, "LANG") catch |err| switch (err) {
        error.EnvironmentVariableNotFound => null,
        else => return err,
    };

    if (lang_var) |l| {
        defer allocator.free(l);
        const result = l[0 .. std.mem.indexOf(u8, l, "_") orelse l.len];
        std.debug.print("{s}\n", .{result});
    }
}

into a function, i.e. I want that function to return the string “en”. With this attempt:

const std = @import("std");

fn language() ![]const u8 {
    var debug_allocator = std.heap.DebugAllocator(.{}){};
    defer std.debug.assert(debug_allocator.deinit() == .ok);
    const allocator = debug_allocator.allocator();

    const lang_var = std.process.getEnvVarOwned(allocator, "LANG") catch |err| switch (err) {
        error.EnvironmentVariableNotFound => null,
        else => return err,
    };

    if (lang_var) |l| {
        defer allocator.free(l);
        const result = l[0 .. std.mem.indexOf(u8, l, "_") orelse l.len];
        return result;
    }

    return "en";
}

pub fn main() !void {
    const lang = try language();
    std.debug.print("{s}\n", .{lang});
}

I get “thread 146758 panic: reached unreachable code”. After debugging, it seems that the defer in the if block frees the memory of l which in turn frees the memory of result such that it becomes the empty string "". How can I make the language function return "en"?

edit: I am porting a project to Zig 0.14 for learning purposes, so this is code that was supposed to work on Zig 0.9.1 or 0.10. Please let me know if you spot an older pattern and if you recommend a newer pattern.

You should create the DebugAllocator inside main, and pass it into your function. Then the result can be freed and the allocator deinitialized in main.

2 Likes

There are two problems. The first, as @n0s4 points out, is that the allocator only lives inside the language() function. Memory allocated by a DebugAllocator can’t normally outlive the allocator itself. If it does, you have a memory leak and will fail the assert(debug_allocator.deinit() == .ok) assertion.

Instead, initialize and deinitialize the allocator in your main() function and pass it as an argument to language().

The second problem is that you free the environment variable value before the substring is returned, which means that result will point to freed memory. To fix this, you could use the allocator to dupe the substring and return that duped string instead, while still freeing the original environment variable value.

I would suggest the following changes:

const std = @import("std");

fn language(allocator: std.mem.Allocator) ![]const u8 {
    // return the default value directly from the 'catch'
    // to make the memory management a little easier to read
    const lang_var = std.process.getEnvVarOwned(allocator, "LANG") catch |err| switch (err) {
        error.EnvironmentVariableNotFound => {
            // it's important that you don't return a string literal
            // the program might crash if the caller tries to free a string literal
            // instead, dupe it into a new allocation
            return allocator.dupe(u8, "en");
        },
        else => return err,
    };
    defer allocator.free(lang_var);

    // use 'sliceTo()' to slice to a specific character or the end of the string
    return try allocator.dupe(u8, std.mem.sliceTo(lang_var, '_'));
}

pub fn main() !void {
    // use '= .init'
    // initializing 'DebugAllocator' with '= .{}' is deprecated
    var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
    defer std.debug.assert(debug_allocator.deinit() == .ok);

    const allocator = debug_allocator.allocator();

    const lang = try language(allocator);
    defer allocator.free(lang); // don't forget to free!

    std.debug.print("{s}\n", .{lang});
}

There’s one minor optimization you could make by avoiding duplicating the environment variable value if it doesn’t contain a '_', but it’s unlikely to matter and makes the code more complicated. I’ll leave it as an exercise to the reader if you really want to try :slightly_smiling_face:

5 Likes

@n0s4 thanks for the suggestion! When I do this, then how do I free lang_var in main?

with .free using the same allocator you passed to the function, preferably with a defer right after the function call

@vulpesx It then says invalid free, because I do not have a way to free a variable internal to language.

@castholm thanks for the suggestions and code example! With it, I found my mistake: I removed the .frees in language, but your examples showed me that they have to stay there.

that makes no sense, you are freeing the memory that the function returns.

can you provide the actual error message and traces

ah, thats because you need to free the original memory that the env var gets stored in, but not the new duped memory that you want to return

Yes, exactly. edit: I guess the new duped memory should and is also freed in Defer and function return - #3 by castholm.