Loading game map data from a file into a [dynamic]string

First of all, sorry for the excessive verbosity. TL;DR: No work, help plz!

I’m on current Fedora, Odin version is dev-2025-02-nightly. I meant to download the current stable at the time, maybe I made a mistake.

I’m trying to learn Odin by rewriting Pygame Tutorial #3: Tile-based game by KidsCanCode in Odin with raylib. Are there better ways to learn Odin? Sure, but this is what I chose to do.

Since I know don’t know Odin, my idea is to stick as close to the original as possible in the first iteration (bearing in mind that Python and Odin are rather different), and make it more “Odinesque” (?) later.

The first part went fine. I found some tutorials on Odin and raylib to help me out. Now I’m on part 2 and am a bit stumped on something. I know I’m doing something wrong and it sure looks like a memory-related issue, but I’m can’t figure it out, so I’m hoping to get an explanation as to what I’m doing wrong.

I created the following procedure. It reads in a file that consists of two characters (runes): ‘1’ and ‘.’, where ‘1’ indicates a “wall” and ‘.’ nothing.

load_data :: proc(md: ^[dynamic]string) {
	data, ok := os.read_entire_file("./map.txt", context.allocator)
	if !ok {
		return
	}
	defer delete(data, context.allocator)

	it := string(data)
	for line in strings.split_lines_iterator(&it) {
		append(md, line)
	}
}

In the main proc, I call this to load in the map:

map_data: [dynamic]string
defer delete(map_data)

load_data(&map_data)

for row, y in map_data {
	for col, x in row {
		if col == '1' {
			append(&wall_group, Wall{{f32(x), f32(y)}, rl.GREEN})
		}
	}
}

Wall is just a struct:

Wall :: struct {
	pos: rl.Vector2,
	fill: rl.Color,
}

Now this works, EXCEPT for the first 8 characters/runes/cells of the first row. They get filled with some random characters. Every time I run, it’s the first 8 cells of the first row, which is why it smells like some memory allocation thing that I am unaware of.

If I print out each row inside the load_data proc, then the first row is correct, it’s all 1s. However, after returning to main, those first 8 runes are damaged.

Another odd thing: if I move the body of load_data into main (renaming variables as needed), then it works fine, no runes get corrupted.

I’d be very grateful if somebody could explain what I’m doing wrong here, because I can’t figure it out. If somebody wants to also point out a better way to do this, I’ll of course be happy about that as well, but I would still want to understand what I’ve done wrong and how to fix it.

If it would help to have access to the code: https://codeberg.org/lorenzocabrini/zombie-shooter

You are deallocating all the strings here

defer delete(data, context.allocator)

Remove that line, and instead deallocate the strings in the map in the main function.

Thank you for the response. You were right, but I don’t understand exactly why? Would you mind walking this through with me?

I was of the impression that the defer delete statement related purely to the temporary data variable and that, by the time the load_data proc runs all the strings would have been appended to map_data and thus not needed anymore. Your answer indicates that I have totally misunderstood this, however.

What really confused me was the fact that it was always the first 8 chars/runes (sorry, I’m old and grew up in a wolrd of ASCII) that got corrupted. I guess that is due to some freeing of memory that then gets used be something else.

In any case, how do I mark this as resolved? I see a “fixed” tag, do I simply apply that?

It seems that you imagine that the strings will be copied/cloned from the “temporary” data variable. This is not the case. If you look at package strings - pkg.odin-lang.org you will see that this function doesn’t take an allocator in the parameter list. This is a hint that this function will not clone the strings. It will just return pointers into the original string (the data variable).

So the memory that data points to, is the memory being used all along.
Why is only the first 8 bytes garbled, and not the whole string? It is because freeing memory will not “clean” it - it won’t be overwritten with zeroes for example. It will be left there intact, but it could be overwritten by anything else later on.
It seems, in your case, the first 8 bytes is being overwritten.

1 Like

Now I get it. It took a while. It’s been a bit too much of high-level languages for a few years, so I’ve managed to lose track of how memory management works.

Thanks a lot for your input.

odin test . is reporting this use case as a memory leak.

From my understanding this shouldn’t be leaking memory since the allocated memory is being deleted in the main proc.

I am able to cleanly get the data from the file and do the proper processing on it, I am just concerned that reading a file in using this proc will leak memory in a longer running program. Is there something that I am doing wrong here?

package helper

read_file_into_new_array :: proc(filepath: string) -> [dynamic]string {
	data, ok := os.read_entire_file(filepath)
    /* defer delete(data) */
	if !ok {
		fmt.println("Unable to read file")
		return make([dynamic]string)
	}

	it := string(data)
	out := make([dynamic]string)
	for line in strings.split_lines_iterator(&it) {
		if len(line) != 0 {
			append_elem(&out, line)
            /* defering delete(data) and using strings.clone(line)
               here marks the clone as leaked */
		}
	}

	return out
}
package main

main :: proc () {
	data: [dynamic]string = helper.read_file_into_new_array("in.txt")
	defer delete(data)

    /* do some other stuff */
}
@(test)
test_main :: proc(^testing.T)  {
	main()
}
$ odin test .
[INFO ] --- [2025-07-01 13:13:51] Starting test runner with 1 thread. Set with -define:ODIN_TEST_THREADS=n.
[INFO ] --- [2025-07-01 13:13:51] The random seed sent to every test is: 16642247443410. Set with -define:ODIN_TEST_RANDOM_SEED=n.
[INFO ] --- [2025-07-01 13:13:51] Memory tracking is enabled. Tests will log their memory usage if there's an issue.
[INFO ] --- [2025-07-01 13:13:51] < Final Mem/ Total Mem> <  Peak Mem> (#Free/Alloc) :: [package.test_name]
[WARN ] --- [2025-07-01 13:13:51] <  13.67KiB/  44.55KiB> <  29.55KiB> (    7/    8) :: main.test_main
        +++ leak   13.67KiB @ 0x130420038 [file_reader.odin:8:read_file_into_new_array()]
main  [|                       ]         1 :: [package done]

Finished 1 test in 1.047ms. The test was successful.

The problem is here - data, ok := os.read_entire_file(filepath)
You have to delete data because it’s allocating a []byte

Thank you for the reply, but this was only part of my problem and I was able to get help on the discord :slight_smile:

What I wanted to do was to transfer ownership of the []byte from the helper method into the main proc through the [dynamic]string, but this thought process was incorrect.

I could not delete(data) in the helper proc otherwise the [dynamic]string would be replaced with garbage after creating memory for other things in my program.

I could not delete(data) in my main proc, since the [dynamic]string was referencing a transformation of the data rather than owning the data it was based on, so a delete here was a bad free.

I ended up having to settle on cloning the strings, which I was trying to avoid for efficiency. The final product of my code now has 0 memory issues and looks like this

read_file_into_new_array :: proc(filepath: string) -> [dynamic]string {
	data, ok := os.read_entire_file(filepath)
	defer delete(data)
	if !ok {
		fmt.println("Unable to read file")
		return make([dynamic]string)
	}

	it := string(data)
	out := make([dynamic]string)
	for line in strings.split_lines_iterator(&it) {
		if len(line) != 0 {
			append_elem(&out, strings.clone(line))
		}
	}

	return out
}
main :: proc() {
	lines := helper.read_file_into_new_array("in.txt")
	defer  {
		for &s in lines {
			delete(s)
		}
		delete(lines)
	}
}
$ odin test .
[INFO ] --- [2025-07-02 21:56:53] Starting test runner with 8 threads. Set with -define:ODIN_TEST_THREADS=n.
[INFO ] --- [2025-07-02 21:56:53] The random seed sent to every test is: 2010667595596. Set with -define:ODIN_TEST_RANDOM_SEED=n.
[INFO ] --- [2025-07-02 21:56:53] Memory tracking is enabled. Tests will log their memory usage if there's an issue.
[INFO ] --- [2025-07-02 21:56:53] < Final Mem/ Total Mem> <  Peak Mem> (#Free/Alloc) :: [package.test_name]
main  [||||||||                ]         8 :: [package done]

Finished 8 tests in 2.914ms. All tests were successful.

I am still very new to Odin, so if I come across a way to do this without cloning the strings and without carrying around the entire file with the [dynamic]string then I will post an update here

There are a few solutions. The simplest is to use an arena allocator and then just delete the arena at the end of the program.

package main

import "core:fmt"
import "core:mem"
import vmem "core:mem/virtual"
import "core:os"
import "core:strings"

main :: proc() {
	arena: vmem.Arena
	err := vmem.arena_init_static(&arena, 2 * mem.Megabyte) // could also use a growing arena if you want
	assert(err == .None)
	arena_allocator := vmem.arena_allocator(&arena)
	defer vmem.arena_destroy(&arena) // frees all the memory allocated at once

	file := read_file_into_new_array("in.txt", arena_allocator)
	fmt.println(file)
}

read_file_into_new_array :: proc(filepath: string, arena_allocator: mem.Allocator) -> [dynamic]string {
	data, ok := os.read_entire_file_from_filename(filepath, arena_allocator)

	if !ok {
		fmt.println("Unable to read file")
		return make([dynamic]string, arena_allocator)
	}

	it := string(data)
	out := make([dynamic]string, arena_allocator)
	for line in strings.split_lines_iterator(&it) {
		if len(line) != 0 {
			append_elem(&out, line)
		}
	}

	return out
}
1 Like

This is fantastic thank you! I don’t have much experience with arena allocators coming from an RAII background. I feel like this will be very helpful to use in a lot of places