DEV Community

loading...

Validating strings at compile time in Zig

Euan T
Software engineer working on messaging middleware systems and integration.
・4 min read

The scenario is simple, and unfortunately quite common when interfacing with libraries designed for C: you have a library function which takes a single string parameter which can only contain a list of pre-defined values.

Take, for example, the pledge(2) system call from OpenBSD. This system call is quite simple, and takes two parameters — both of which are strings that may only contain a pre-defined list of promises.

Passing a string with an invalid promise value (such as a spelling mistake) will result in an error at runtime. Wouldn't it be nice if we could instead catch such an issue at compile time?

That's exactly what we're going to do, using the Zig programming language.

Note: this is extracted from a blog post on my site, which looks at some alternative approaches in a couple of other languages

What is Zig?

Zig is described as:

Zig is a general-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.

It is a language with a C-like syntax that aims to replace C. It brings some useful tools to the table, including comprehensive support for running code at compile time.

It's this functionality that we will be using in this post.

Defining the problem

Our problem is going to take some queues from the pledge(2) system call, though we are going to simplify it a bit:

  • In this case, the system call will only have 1 string parameter.
  • In this case, there will be drastically fewer promises available:
    • stdio
    • rpath
    • wpath
    • path
    • dpath
  • Our function signature should look something like the following:

    int pledge(const char *promiseList);
    
  • The promiseList parameter should be a string containing only the promises set of available promises, separated by a single space.

Writing some Zig

Let's write some code!

Creating an enum to represent the available promises

As we have a limited set of pre-defined promises, the easiest way to represent these values is as an enum.

Let's start off by defining one, named Promises:

const Promises = enum {
    stdio,
    rpath,
    wpath,
    path,
    dpath,
};

This should be quite familiar to most with some experience using c-like languages such as C# or Java.

Writing a function to validate a string of promises

We should probably create a function that will take a string of promises separated by spaces and ensure all the values are valid, well-known values.

Luckily, Zig's standard library has some functions to help with this:

We can use these functions to iterate through all sub-values in a promises string and try to parse them as members of the Promises enum:

const std = @import("std");
const tokenize = std.mem.tokenize;
const stringToEnum = std.meta.stringToEnum;

fn validatePromises(promises: []const u8) bool {
    var splitter = tokenize(promises, " ");

    while (splitter.next()) |s| {
        _ = stringToEnum(Promises, s) orelse return false;
    }

    return true;
}

If any of the values within the promises string fail to parse, we return false. However, if they all parse successfully, we return true.

Calling validatePromises at compile time

This is where the magic happens. We now have a way to validate a promises string, but we need to call it at compile time.

Luckily, Zig has an awesome comptime concept. We can easily declare that certain arguments passed to functions should be known at compile time, and run whole blocks of code at compile time using those parameters. For example:

const warn = std.debug.warn;

fn promise(comptime promises: []const u8) void {
    comptime {
        if (!validatePromises(promises)) {
            @compileError("invalid promises: " ++ promises);
        }
    }

    warn("valid promises: {}", .{promises});
}

This code will call the validatePromises function at compile time with the passed in promises string. If the validation fails, a compiler error is emitted, including the invalid promises.

This results in a build failure like the following:

./src/main.zig:28:13: error: invalid promises: stdio foobar
            @compileError("invalid promises: " ++ promises);
            ^
./src/main.zig:37:16: note: called from here
        promise("stdio foobar");
               ^
./src/main.zig:35:29: note: called from here
pub fn main() anyerror!void {
                            ^
app...The following command exited with error code 1:

...

Putting it all together

I've only scratched the surface of what's possible with Zig's comptime facilities here, but I hope I've given you some inspiration to give it a try.

I'd love to see what other ideas you think up with this kind of approach. For now, I've got a simple repository available with the working source code for this Zig implementation, along with a Nim version and a D version.

Discussion (0)