Dynamic Array of Strings getting corrupted on return from proc

I am trying to learn Odin for Advent of Code and have run into a problem reading a file line by line in a function. I have tried returning the array by value and returning a slice. Each results in the same problem… The first two elements of the array get corrupted by random garbage. Here is my code:

package main

import "core:os"
import "core:fmt"
import "core:slice"
import "core:strings"

read_file_line_by_line :: proc(filepath: string, lines: ^[dynamic]string ) {
	data, ok := os.read_entire_file(filepath, context.allocator)
	defer delete(data, context.allocator)

	it := string(data)
    fmt.println( "As the array is built...")
	for line in strings.split_lines_iterator(&it) {
		append( lines, line )
        fmt.println( line )
	}

    fmt.println( "After the array is built...")
    for line in lines {
        fmt.printf( "%s\n", line )
    }
}

main :: proc() {
    if len(os.args) < 2 {
        fmt.eprintf( "Usage: %s <filename>\n", os.args[ 0 ] )
        return
    }

    lines : [dynamic]string
    read_file_line_by_line( os.args[ 1 ], &lines )

    fmt.println( "After the array is returned...")
    for line in lines {
        fmt.println( line )
    }
}

And my input is a file containing:

L68
L30
R48
L5
R60
L55
L1
L99
R14
L82

My output is:

As the array is built...
L68
L30
R48
L5
R60
L55
L1
L99
R14
L82
After the array is built...
L68
L30
R48
L5
R60
L55
L1
L99
R14
L82
After the array is returned...
�`     << Corrupted element
�f�    << Corrupted element
R48
L5
R60
L55
L1
L99
R14
L82

I am using odin-linux-amd64-nightly+2025-12-0 on Ubuntu 24.04 running on WSL2.

Any help would be appreciated. Thank you in advance.

lines is an array of strings. Every string is basically a pointer to a memory location. Those pointers are allocated in append(lines, line). The actual data that the pointers point to is allocated in os.read_entire_file and freed in delete(data). Thus, your strings point to freed memory.

2 Likes

You could just move the reading and deleting to the outer scope and pass the data slice to the function to solve this.

Also: Welcome to the forum :v:

2 Likes

OK. Thank you. I was somehow thinking that the following line did a deep copy of the data… e.g. converting from bytes to string would trigger a utf8 decode.

So if I remove defer delete(data, context.allocator) that should solve my problem.

In my development of the code above I was partly basing my approach on this example. Might be helpful to modify this example to include the idiomatic way of reading a file line by line and returning an array of strings. Seems like a very common work flow.

2 Likes

The thing is that it is idiomatic to free allocated memory. If you just remove the delete, you no longer do that. In the case of AoC, that’s obviously no problem because you only read one file in the entire program’s lifetime. If you read an arbitrary amount of files, you’ll want to clean that up. One more generalized way would be to use the context.temp_allocator for the read_entire_file. Then, the caller would just call free_all on that at some later point which you usually do anyway in longer running programs.

1 Like

I saw this as an opportunity to share what I’ve learned. Hopefully it helps. I welcome others to correct me where I’m wrong.

Since the procedure stack for a particular procedure is cleared when it exits, and Odin will let you shoot yourself in the foot with memory allocations and frees(which can be a good thing once you get the hang of it), passing dynamic elements and strings around can produce some non-intuitive situations. If you’ve got a long running program, or plan to modify strings and/or memory through several interlocking steps, then things can get weird fast.

Consider the approach.

  • The most straight forward approach. Acquire Data, Use Data, Free Data, all in the same context. Hard to follow when your program gets complicated, but makes it much easier to know who owns what memory, and when to clear it.
  • If you need to pass dynamic memory around, then I highly recommend adding a memory tracker to your program for debug purposes. It helps a lot. I link this at the bottom.
  • An Arena Collector is another way to go, but personally, I’m forcing myself to not use those until I get a full handle on memory management myself.

The simple approach: Acquire data, Use Data, Free Data

main :: proc() {
	if len(os.args) < 2 {
		fmt.eprintf( "Usage: %s <filename>\n", os.args[ 0 ] )
		return
	}

	data, ok := os.read_entire_file(os.args[1])
	defer delete(data)

	lines := strings.split(string(data), "\n")
	defer delete(lines)

	for line in lines {
		fmt.printfln("%s", line)
	}
}

An approach using the example, but modified to allow the data to live past the clearing of the procedure stack and the “delete(data)”, that also fully frees memory when done with it. This illustrates how some non-intuitive situations arise from acquiring data in a different context than it is used. Note how each individual string in the dynamic array must be freed in addition to the dynamic array itself. Also, the addition of strings.clone to make a copy of the data before it gets cleared.

read_file_line_by_line :: proc(filepath: string, lines: ^[dynamic]string) {
	data, ok := os.read_entire_file(filepath, context.allocator)
	defer delete(data, context.allocator)

	it := string(data)
	for line in strings.split_lines_iterator(&it) {
		append( lines, strings.clone(line) ) // clone string so it can live past procedure stack when data is deleted
	}
}

main :: proc() {
	if len(os.args) < 2 {
		fmt.eprintf( "Usage: %s <filename>\n", os.args[ 0 ] )
		return
	}

	lines: [dynamic]string
	defer delete(lines) // free pointers created by append (this must happen last, so the defer is defined first)
	defer for line in lines { // free strings cloned in procedure (this must happen before freeing the pointers)
		delete(line)
	}

	read_file_line_by_line( os.args[ 1 ], &lines )

	for line in lines {
		fmt.println( line )
	}
}

The example modified to use GigaGrunch’s suggestion. Allows to pass the data from a procedure and more intuitive. It’s more clear where and when to clear the memory.

read_file_line_by_line :: proc(filepath: string, buf: ^[]byte, lines: ^[dynamic]string) {
	buf^, _ = os.read_entire_file(filepath)

	it := string(buf^)
	for line in strings.split_lines_iterator(&it) {
		append( lines, line )
	}
}

main :: proc() {
	if len(os.args) < 2 {
		fmt.eprintf( "Usage: %s <filename>\n", os.args[ 0 ] )
		return
	}

	buf: []byte
	defer delete(buf)
	lines: [dynamic]string
	defer delete(lines)

	read_file_line_by_line( os.args[ 1 ], &buf, &lines )

	for line in lines {
		fmt.println( line )
	}
}

Memory Tracker. After adding this to your program, you can view it’s data when you compile with “odin . -debug”

1 Like