Skip to content

Instantly share code, notes, and snippets.

@markkimsal
Last active July 3, 2023 12:50
Show Gist options
  • Save markkimsal/0d422071bec4b35907764f6190b76f7b to your computer and use it in GitHub Desktop.
Save markkimsal/0d422071bec4b35907764f6190b76f7b to your computer and use it in GitHub Desktop.
Porting Super Cube Slide to Zig

I've had my eye on Zig for a year or so now as a language I'd like to learn. I don't have much use for compiled languages in my normal day-to-day work, so it's challenging to find time to learn new languages.

I remembered that I have an old Python game, Super Cube Slide, that I haven't been able to get building for Windows and have not had great success porting to Python 3.x. After finding a nice SDL wrapper in Zig (https://github.com/MasterQ32/SDL.zig) I thought I'd give a shot at porting to Zig. Hopefully this should let me redistribute binaries to friends and family much easier than setup.py and friends.

A few things about Zig that I noticed right away. There's not much documentation on managing your allocators, and there's no package management system. Lack of package management system kept me away from Go-lang in the early days. In fact, I ported an app away from Go to PHP because the previous dev left it in such an unmanagable state and Go-lang was tossing back and forth between two different packaging systems.

Lack of allocator documentation is probably going to be the the biggest stumbling block, especially since I'll be integrating heavily with C libraries. I don't really know what I'm doing with manual memory management, so we'll see how it goes :)

SDL v Raylib

I was going to use Raylib, as I saw an ffmpeg player done in zig that uses raylib. But, my original python is in SDL and I figured that would be setting myself up for failure to switch languages and libraries at the same time.

Rust v Zig

I did the rustlings last December and have been working on porting some programs to Rust, an ffmpeg player and a tax retirement engine. The ffmpeg player was no fun because 10% of my Rust code is the word unsafe. I want to give Zig a shot for projects that heavily depend on existing C libraryes.

Zig Documentation

Is it just me, or does Zig's documentation contain zero structs or functions that hold or take a string? Is it []u8? [] const u8? [] *const u8? There are no examples anywhere.

I'm going to go with [*c]const u8 but I don't know what it means. This is from dumping SDL_mixer.h with zig translate-c.

Zig Memory Management

The promise of the compiler never managing memory for you sounds intriguing at first, until you realize that concating strings requires 3-4 lines of code - minimum.

    const filename = song_list[song_index].filename;
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const final_url = std.fmt.allocPrint(arena.allocator(), "../media/music/{s}", .{filename}) catch {return;};
    var song : ?*mixer.Mix_Music = mixer.Mix_LoadMUS(@ptrCast([*c]const u8, final_url));

Thoughts on Zig

I think zig is solid 11/10 for integrating with existing C libraries and headers. You can't really ask for more.

I do wish the documentation had more examples, perhaps aimed at new coders - or at least coders new to compiled languages and heap vs stack memory.

I want to read this article and follow along at some point https://mht.wtf/post/comptime-struct/

Day 2 Update

Wow, ok. I guess I don't understand how to program. I tried to make some structs for different game modes, like help screen, timed attack, credits, etc. You can't (easily) have types that behave the same way: call it inheritance, interfaces, dynamic dispatching... You can't help but make the comparison to Rust traits.

Issue asking for vtable support There seems to be some common desire for some way to merge behaviors under some umbrella type. It seems like the standard library uses some kind of vtable trick in a couple places. I can understand the desire to keep such an Interface package like this one out of the core language, but why not expose it as part of the standard library? Hopefully the package management system will bring a lot of these useful packages to the foreground.

I also found this article that really explained a way to do some dynamic dispatching: https://zig.news/kristoff/easy-interfaces-with-zig-0100-2hc5. This is the method that I'm proceeding with at the moment.

Update 3

I used valgrind today to successfully remove a memory leak (first time, in case you couldn't tell (or at least the earliest time I remember doing it))

==1718401== 48,350 (27,312 direct, 21,038 indirect) bytes in 1 blocks are definitely lost in loss record 922 of 923
==1718401==    at 0x4E050C5: malloc (vg_replace_malloc.c:393)
==1718401==    by 0x48C3CD9: ??? (in /usr/lib/x86_64-linux-gnu/libSDL2-2.0.so.0.18.2)
==1718401==    by 0x49EA898: TTF_OpenFontIndexDPIRW (in /usr/lib/x86_64-linux-gnu/libSDL2_ttf-2.0.so.0.18.0)
==1718401==    by 0x224BFD: ttf.openFont (ttf.zig:107)
==1718401==    by 0x224E3A: modes.attract.AttractMode.init (attract.zig:17)
==1718401==    by 0x2254F9: main.main (main.zig:37)
==1718401==    by 0x226914: callMain (start.zig:609)
==1718401==    by 0x226914: initEventLoopAndCallMain (start.zig:543)
==1718401==    by 0x226914: callMainWithArgs (start.zig:493)
==1718401==    by 0x226914: main (start.zig:508)

At first I thought this was a problem in OpenFont or some other libSDL library, because they were closer to the malloc call and because I was using Zig, and I'm doing everything right, right?

Well, sure enough, adding a simple self.font.close() in the right spot cleared it up. There are other leaks that deal with GLIBC and _dl_open, so, not sure if those are mine. There are 2 or 3 more with nvidia libs, I think it's already reported.

==1719030== 123,502 (896 direct, 122,606 indirect) bytes in 1 blocks are definitely lost in loss record 857 of 857
==1719030==    at 0x4E09EBD: realloc (vg_replace_malloc.c:1451)
==1719030==    by 0x8DDB7B2: ??? (in /usr/lib/x86_64-linux-gnu/libnvidia-glcore.so.470.182.03)
==1719030==    by 0x8DD1B40: ??? (in /usr/lib/x86_64-linux-gnu/libnvidia-glcore.so.470.182.03)
==1719030==    by 0x8DCF1AE: ??? (in /usr/lib/x86_64-linux-gnu/libnvidia-glcore.so.470.182.03)
==1719030==    by 0x8DE13C8: ??? (in /usr/lib/x86_64-linux-gnu/libnvidia-glcore.so.470.182.03)
==1719030==    by 0x744BC78: ??? (in /usr/lib/x86_64-linux-gnu/libGLX_nvidia.so.470.182.03)
==1719030==    by 0x74B1C55: ??? (in /usr/lib/x86_64-linux-gnu/libGLX_nvidia.so.470.182.03)
==1719030==    by 0x744B1EA: ??? (in /usr/lib/x86_64-linux-gnu/libGLX_nvidia.so.470.182.03)
==1719030==    by 0x6EDAD7F: ???
==1719030==    by 0x4006439: call_init.part.0 (dl-init.c:56)
==1719030==    by 0x4006567: call_init (dl-init.c:33)
==1719030==    by 0x4006567: _dl_init (dl-init.c:117)
==1719030==    by 0x518AC84: _dl_catch_exception (dl-error-skeleton.c:182)
==1719030== 
==1719030== LEAK SUMMARY:
==1719030==    definitely lost: 7,176 bytes in 9 blocks
==1719030==    indirectly lost: 145,219 bytes in 803 blocks
==1719030==      possibly lost: 0 bytes in 0 blocks
==1719030==    still reachable: 152,965 bytes in 1,384 blocks
==1719030==         suppressed: 0 bytes in 0 blocks
==1719030== Reachable blocks (those to which a pointer was found) are not shown.

I'm guessing this means only 145k leaked, it doesn't sound too bad, even though that's like an entire floppy disk worth of lost data. ¯\_ (ツ)_/¯

Update 4 - stuck on pointers

I can't find any good information on variable scope and loops. This doesn't seem to work as each element of the array points the the latest data in sprite...

        for (0..10) |x| {
            var field_line = self.*.field.get(@intCast(u32, x));
            if (field_line) |*arr| {
                for (0..10) |y| {
                    var cube_texture = sdl.image.loadTextureMem(renderer.*, cube_a[0..], sdl.image.ImgFormat.png) catch |err| {
                        std.log.err("Cannot load texture: {}", .{err});
                        return;
                    };
                    var sprite: Sprite = Sprite.init(cube_texture, 24, 24);
                    sprite.setPosition(240, 240);
                    arr[y] = &sprite;
                }
                self.*.field.put(@intCast(u32, x), arr.*) catch {};
            }
        }

yeah, this is no fun. Going to pause this until I can figure out how to put pointers in arrays.

https://stackoverflow.com/questions/76396219/correct-way-to-put-pointers-into-an-array-zig-lang

Update 5

So, it turns out I have been so abused by heap-allocating scripting languages and reference counting that I couldn't even function in a situation where the language doesn't provide that. I knew zig didn't offer heap memory without using an allocator, but I just thought the stack based memory would hang around if I kept a reference to it in an array... Anyway, creating a struct on the stack in a loop is undefined behavior... seems like that would be a good thing to cover, but everybody using Zig is appearently uber familiar with C (and libc too) ?

I moved all the sprite creation into the sprite module, and used an arena allocator. This was the setup I originally wanted, but got sidetracked trying to create the memory structures in Zig. The stack loop creation was just an intermediate step anyway and I thought I was using the data structures incorrectly.

I'm actually changing the way everything is displayed and drawn just just be relative grid coordinates, then multiplying by a standard tile size to show on the screen. Don't know why it wasn't done this way in the first place, probably for "speed" to avoid multiplication every frame

Peek-scs-band-2023-06-06 09-06

Update 6

Well, the majority of the game is ported. Most of the issues with Zig come from documentation (lack of it), development versions, large std lib changes. The language itself is pretty cool. The integration with C is transparent. My only issue is the cross compiling.

There is very little to no information on cross-compiling. AndrewRK made a "port" of SDL from CMake to Zig build system, but it doesn't cross compile. Cross compiling SDL myself with cmake seems to work, but i don't have libobjc installed locally. Not sure if I do that will the binaries actually work on a mac? I tried cross compiling to windows with some success, but there's a lot involved.

comptime is still very confusing to me. Sometimes I try to throw the keyword into places and it doesn't work.

pointers to functions and executing those functions is another stumbling block - specifically, pointers to struct methods. (I forget what they're called, not impls, but .... )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment