Trying Zig by Implementing CHIP-8

Posted on October 6, 2025

I have watched the development of Zig way back in the early days when it was just Andrew Kelley posting his development streams. The idea was straight-forward: take the best parts of C, leave behind the warts, and make it easy to use. His enthusiasm for the subject and the progress he was making kept me watching. I’ve been following Zig from the side lines ever since.

Some time ago I decided I wanted to try Zig on something small in scope but serious enough that I would be forced out of tutorials and have to think for myself. I’ve always maintained an interest in older computing platforms and systems so I thought I’d try an emulator. Only I didn’t want to start on the NES or even the GameBoy, I needed something even smaller. I needed CHIP-8!

CHIP-8

It’s a byte-code interpreted programming language that originally started on the COSMAC-VIP hobby computer. It has since been ported to several other platforms.

My system would emulate the CHIP-8 virtual machine which is roughly influenced by the original COSMAC-VIP. It would have to handle the simple 64x32 1-bit display, the keypad input, timers, and the 4KiB of memory.

It’s a good place to start learning emulation because of its simplicity. The specification of the virtual machine can fit on a postcard. There are only 35 instructions to implement! This is enough of a project to fill a couple of weekends to get started and get the basics going but not so much that you will get discouraged and put off finishing it.

As of this writing my emulator, zig8, is not 100% finished yet. It implements the base CHIP-8 opcodes and emulates the display, input, and delay timer. I still need to implement sound, the visual debugger I had planned, and cross-platform builds. But I added a nice retro CRT-effect shader and swap-able color palettes for fun!

Zig

I’ve implemented enough of the emulator to get a feel for Zig. Here’s what I’ve liked so far:

Type System

I have been using C throughout my career as a programmer both professionally and recreationally for 30 years. The implicit conversion rules are burnt into my head. And when working on zig8 one of the most common things I grappled with was explicit conversions.

Where you might see:

uint8_t x = (op & 0xFF00) >> 8;

To take the upper 8 bits out of a uint16_t value, op — in Zig it requires an explicit conversion:

const x: u8 = @intCast((op & 0xFF00) >> 8);

Which we see quite a lot of in the zig8 code base. The type of the value of the expression isn’t promoted for you.

This is a bit of a frustration as it adds a lot of boilerplate to the code. You see it in the OpenGL extension initialization code as well:

// OpenGL Vars
var program_id: gl.GLuint = undefined;
var glCreateShader: gl.PFNGLCREATESHADERPROC = undefined;
var glShaderSource: gl.PFNGLSHADERSOURCEPROC = undefined;
var glCompileShader: gl.PFNGLCOMPILESHADERPROC = undefined;
var glGetShaderiv: gl.PFNGLGETSHADERIVPROC = undefined;
var glGetShaderInfoLog: gl.PFNGLGETSHADERINFOLOGPROC = undefined;
var glDeleteShader: gl.PFNGLDELETESHADERPROC = undefined;
var glAttachShader: gl.PFNGLATTACHSHADERPROC = undefined;
var glCreateProgram: gl.PFNGLCREATEPROGRAMPROC = undefined;
var glLinkProgram: gl.PFNGLLINKPROGRAMPROC = undefined;
var glValidateProgram: gl.PFNGLVALIDATEPROGRAMPROC = undefined;
var glGetProgramiv: gl.PFNGLGETPROGRAMIVPROC = undefined;
var glGetProgramInfoLog: gl.PFNGLGETPROGRAMINFOLOGPROC = undefined;
var glUseProgram: gl.PFNGLUSEPROGRAMPROC = undefined;

// OpenGL Helpers
fn init_gl_extensions() bool {
    glCreateShader = @ptrCast(sdl.SDL_GL_GetProcAddress("glCreateShader"));
    glShaderSource = @ptrCast(sdl.SDL_GL_GetProcAddress("glShaderSource"));
    glCompileShader = @ptrCast(sdl.SDL_GL_GetProcAddress("glCompileShader"));
    glGetShaderiv = @ptrCast(sdl.SDL_GL_GetProcAddress("glGetShaderiv"));
    glGetShaderInfoLog = @ptrCast(sdl.SDL_GL_GetProcAddress("glGetShaderInfoLog"));
    glDeleteShader = @ptrCast(sdl.SDL_GL_GetProcAddress("glDeleteShader"));
    glAttachShader = @ptrCast(sdl.SDL_GL_GetProcAddress("glAttachShader"));
    glCreateProgram = @ptrCast(sdl.SDL_GL_GetProcAddress("glCreateProgram"));
    glLinkProgram = @ptrCast(sdl.SDL_GL_GetProcAddress("glLinkProgram"));
    glValidateProgram = @ptrCast(sdl.SDL_GL_GetProcAddress("glValidateProgram"));
    glGetProgramiv = @ptrCast(sdl.SDL_GL_GetProcAddress("glGetProgramiv"));
    glGetProgramInfoLog = @ptrCast(sdl.SDL_GL_GetProcAddress("glGetProgramInfoLog"));
    glUseProgram = @ptrCast(sdl.SDL_GL_GetProcAddress("glUseProgram"));
    return glCreateShader != null and
        glShaderSource != null and
        glCompileShader != null and
        glGetShaderiv != null and
        glGetShaderInfoLog != null and
        glDeleteShader != null and
        glAttachShader != null and
        glCreateProgram != null and
        glLinkProgram != null and
        glValidateProgram != null and
        glGetProgramiv != null and
        glGetProgramInfoLog != null and
        glUseProgram != null;
}

All of those @ptrCast calls on every line! But then again, this can be rather useful because later, when the platform code calls one of these functions:

const program_id: gl.GLuint = glCreateProgram.?();

We have to use Zig’s Optional Pointers to call the function. I don’t know that this is the best Zig code or the best way to be using this feature but it does mean that we don’t have to remember to check for null function pointers before using them. The compiler will tell you to use .?.

I also like that Zig exhaustively matches switch statements and reports an error for unhandled cases. And that there is no fall-through, as convenient as that can be some times in C… it’s a source of a large number of errors.

Foreign C Code

I think this is a great feature of Zig: there are no foreign function wrappers and code generators (for C). You can namespace, import, and call C code like anything else. Marshalling between Zig and C types requires minimal effort because the conversions are built in.

When I started working on adding the CRT shader to zig8, I was pleased at how straight-forward it was. I started with:

const gl = @cImport({
    @cInclude("GL/gl.h");
});

And was able to add:

const minx: gl.GLfloat = 0.0;
const miny: gl.GLfloat = 0.0;
const maxx: gl.GLfloat = @floatFromInt(display_w);
const maxy: gl.GLfloat = @floatFromInt(display_h);
const minu: gl.GLfloat = 0.0;
const maxu: gl.GLfloat = 1.0;
const minv: gl.GLfloat = 0.0;
const maxv: gl.GLfloat = 1.0;

gl.glBegin(gl.GL_TRIANGLE_STRIP);
gl.glTexCoord2f(minu, minv);
gl.glVertex2f(minx, miny);
gl.glTexCoord2f(maxu, minv);
gl.glVertex2f(maxx, miny);
gl.glTexCoord2f(minu, maxv);
gl.glVertex2f(minx, maxy);
gl.glTexCoord2f(maxu, maxv);
gl.glVertex2f(maxx, maxy);
gl.glEnd();

No tooling required other than updating the build.zig file to tell the compiler to link the OpenGL library:

exe.linkSystemLibrary("opengl");

Drawbacks

Zig is evolving fast and isn’t stable yet. Especially the standard library. You can expect that most blog posts, unofficial documentation, and tutorials you find online to be out of date and incompatible with the latest version of Zig. It took me quite a while to figure out the file system API after getting frustrated with out-of-date tutorials and blog posts. For now you should expect to get used to reading the official documentation, source code, and asking questions in the community forums if you need to.

If you are new to systems programming and languages with manual memory management I would recommend you proceed with caution here: self learning is going to be difficult when the ground is constantly shifting underneath you. You might want to rely on a stable language like C for now until you understand the fundamentals. Knowing how C works will give you a better appreciation for what Zig offers.

Conclusion

There are a few more features I would like to add to zig8 before I’m done. I am enjoying my time with Zig and hope that the language and community will continue to grow. There’s a lot of space for improvement on C-like systems programming languages and I think Zig is headed in a good direction. I would be happy to use it more.