[Outreach] Looking for a simple example of using comptime instead of macros

I’m trying to convince C people to have a look at Zig and some regret the lack of macros. I know that the standard answer is “we have comptime instead” but all the examples I’ve seen seem complicated and do not really convince anynone. So, I’m looking for simple examples where a programming problem which would have been handled by a macro in C is done with comptime (and a few other things).

2 Likes

This is simple and pretty cool to me:

And bonus points that I can use it in a switch statement:

const hash = std.hash.Fnv1a_32.hash;

    write(switch (hash(builtin_type)) {
        hash("void") => "void",
        hash("char") => "c_char",
        ...
3 Likes

I think it is difficult for me to come up with an example, because I can’t even come up with an example where you would want to use a c macro, to me they just seem like a bad hack from the past and I don’t really recognize them as a valid form of meta programming.

I can understand why people use them in c, for example to hide ugly repetitive pointer casting / access patterns or things like that, but even there it seems like a bad solution to missing language features. In zig it seems unnecessary to have these syntactic hidings, instead you have semantic ways to avoid these (or can use comptime), in general I am not that interested in syntactic meta-programming anymore (making things look nice by adding syntactic abstractions) and more interested in semantic meta-programming let me express something without inventing new syntax.

Take what I am saying with a grain of salt I haven’t thought about it in depth and hopefully the essence of what I mean comes across.

In the lisp world there are 2 kind of macros (very different from c’s text preprocessor replacements), but they also resemble this kind of difference in a way, you can have either unhygenic macros or hygenic ones, the former can easily have unintended consequences similar to when you aren’t careful with c macros, the latter do a whole bunch of housekeeping to make sure your macro doesn’t accidentally capture some variable that was from some other unrelated scope but happend to have the same name and similar errors.
I have written some racket macros and for a while I was fond of macros and doing lots of meta programming, however I think with zig I found that I don’t really care about macros anymore at all. For all the easy kinds of meta programming, comptime seems to be capable of all I want to do, basically do some generic things, do something for these fields, etc. and for more complicated things I think I actually like having to define your own buildstep if you want to invent crazy DSLs and complicated trickery.

One thing I didn’t like about lisp is that it fragmented the community, everyone invents their own syntactic variations for trivial little things to make things look nicer and usually those newly invented things are somewhat half-baked and become awkward when you want to use them together with someone else’s inventions, in the end you end up with a crazy mess. Sure you can apply discipline and make decisions about whats allowed and what isn’t, but all of that costs time and energy, it is nice to have a base syntax that just dictates this is what you can do and everything beyond that, can be seen as some project specific feature implemented via a buildstep.

Another thing I didn’t like about lisp macros is the big divide between macros and functions, with comptime that seems a lot more seamless where a lot of functions can be used at comptime or runtime, without me even having to think about it a whole lot.
With racket you always had to learn more and more as your macro got more complicated, it ends up being a distraction allowing you to waste time instead of making you more productive. (Different kinds of macros, different utilities, pattern based macros, pattern based macros that declare syntax classes (basically kinds of allowed syntax), macros that define macros, etc.).

So I think one of the strengths of comptime is that it gives you reasonable bounds on what you can do with it. Where many other meta programming tools are so general and unbounded, that they place a big burden of responsibility on the community and make it very easy to be used in a way that “does too much or unnecessary meta-programming”.

tl;dr
maybe c macros shouldn’t be used in the first place?
are there good c macros? (that wouldn’t be better served by some actual language feature?)

5 Likes

the punishment for this blasphemy is to have to continue to use C macros :^)

7 Likes

Here’s a few that might be compelling:

Simple type safe generic queue: Code Examples ⚡ Zig Programming Language
(compare to e.g. OpenBSD’s queue: src/sys/sys/queue.h at master · openbsd/src · GitHub)

Switching on target OS using normal Zig code (the value of clock_id is known at compile time):

(in C this would have to use #if and #define)

4 Likes

How about something like this:

const std = @import("std");

fn NoPrivate(comptime T: type) type {
    const fields = @typeInfo(T).Struct.fields;
    var new_fields: [fields.len]std.builtin.Type.StructField = undefined;
    var count = 0;
    inline for (fields) |field| {
        if (field.name[0] != '_') {
            new_fields[count] = field;
            count += 1;
        }
    }
    return @Type(.{
        .Struct = .{
            .layout = @typeInfo(T).Struct.layout,
            .decls = &.{},
            .fields = fields[0..count],
            .is_tuple = false,
        },
    });
}

fn removePrivate(s: anytype) NoPrivate(@TypeOf(s)) {
    const fields = @typeInfo(@TypeOf(s)).Struct.fields;
    var result: NoPrivate(@TypeOf(s)) = undefined;
    inline for (fields) |field| {
        if (field.name[0] != '_') {
            @field(result, field.name) = @field(s, field.name);
        }
    }
    return result;
}

test "removePrivate" {
    const Struct = struct {
        hello: i32 = 456,
        world: i32 = 777,
        _cowbell: i32 = 1234,
    };
    const s: Struct = .{};
    const r = removePrivate(s);
    std.debug.print("\n{any}\n{any}\n", .{ s, r });
}

The function above returns a struct with all fields starting with an underscore removed. Do this in C wouldn’t be simple, I think.

1 Like

I’ll just throw in my two cents here (I largely agree with what people are saying here).

It’s not your macros that are the problem… it’s the macros that everyone else writes that are the problem :slight_smile:

I was at work the other day and I wanted to get the max() int value for reasons unrelated to this. Well, whoops… someone defined a max macro. Just called it max like it was no big deal or anything…

I had to search for a place above where the macro was being freighted in and create a constant variable there.

The real problem with macros is that they allow each programmer to make a sub-language that doesn’t follow the typical rules of the parent language. I could go on, but I won’t lol.

6 Likes

Thanks to everyone, I did not flagged a solution since I had not a specific problem but all the answers are useful.

3 Likes

I (as a long time C coder) managed to convince myself by using comptime for implementing generic data structures. In C we have basically two possibilities:

  • generic pointers (void*, a pointer to anything) - this is type unsafe, however
  • macros - this is type safe

I’m attaching a file (fifo.h) in which I implemented generic FIFO queue some time ago.
Using Zig comptime similar things can be done in much more elegant way.
fifo.h.txt (1.2 KB)

2 Likes

On a side note, I just added the extensions .c , .h, .cpp, and .zon to the allowed files of the forum so you don’t have to add the .txt anymore. :^)

Cool example in C, thanks.

Most likely, I wrote it (and some other generics like dynamic array) after reading this nice post. Maybe I will post full fifo example in C later, but now I’d like to complete my answer with the (almost) same thing implemented in Zig:

const std = @import("std");
const Allocator = std.mem.Allocator;

fn Fifo(comptime T: type) type {

    return struct {

        const Self = @This();
        const FifoItem = struct {
            data: T,
            next: ?*FifoItem,
        };

        a: Allocator,
        head: *FifoItem = undefined,
        tail: *FifoItem = undefined,
        nitems: usize = 0,

        fn init(a: Allocator) Self {
            return Self {
                .a = a,
            };
        }

        fn put(self: *Self, item: *const T) !void {
            var fi = try self.a.create(FifoItem);
            fi.data = item.*;
            if (0 == self.nitems) {
                self.head = fi;
            } else {
                self.tail.next = fi;
            }
            self.tail = fi;
            self.nitems += 1;
        }

        fn get(self: *Self) ?T {
            if (0 == self.nitems) return null;
            self.nitems -= 1;
            var fi = self.head;
            self.head = fi.next.?;
            var item = fi.data;
            self.a.destroy(fi);
            return item;
        }
    };
}

const Message = struct {
    a: u64 = 0,
    b: bool = false,
};

const Queue = Fifo(Message);

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.print("leakage?.. {}\n", .{gpa.deinit()});
    var mq = Queue.init(gpa.allocator());
    var messages: [3]Message = .{
        .{.a = 3},
        .{.a = 2, .b = true},
        .{.a = 1, .b = true},
    };

    for (messages) |m| {
        try mq.put(&m);
    }

    while (true) {
        var m = mq.get() orelse break;
        std.debug.print("{any}\n", .{m});
    }
}

It is a working example:

zig-lang$ ./fifo 
fifo.Message{ .a = 3, .b = false }
fifo.Message{ .a = 2, .b = true }
fifo.Message{ .a = 1, .b = true }
leakage?.. heap.general_purpose_allocator.Check.ok

What I want to say is.

  • I guess there are many C programmers around a world who do not even suspect what clever things can be done with… well, macro-acrobatics :slight_smile:
  • It’s not that I consider Zig way of doing generics absolutely the best, but still it is, at least, more convinient than C-preprocessor macro “style”.
4 Likes

generics-c-vs-zig.tar.gz.txt (2.0 KB)

Ok, here is an implementation of generic fifo queue in C (macro kung fu) and in Zig (comptime)
(after downloading just rename the file to generics-c-vs-zig.tar.gz).
Now you can compare :slight_smile:
Hope, these examples are not extremely complex.

Some random thoughts about the topic (“how to convince C people to try Zig”):

  • there are no default values for struct fields in C, it’s almost always useful
  • there are no optionals in C (hence interfaces in the examples are a bit different)
  • if you really need something like void*, no problem at all - there is *anyopaque which you can cast here and there to whatever you want
1 Like

C macros is a fairly wide topic that can range from conditional compilation, to constants, to quite complex code. What exactly is your team referring to?

Constants are are done with pub const. Code is done w/ fictions (comptime or not).

Conditional compilation is done w/ the build system. In your code you end up w/ a const, and use normal if statments.

so this:

#ifdef WIN32
...
#endif

becomes

if(is_win32) {
...
}
1 Like

IMHO, one of the biggest pain points with C is the zero-terminated string. I’m sure most C programmers have encountered this issue: You’re given a file path and you want the directory part. You have to provide new storage for the string just so you can stick in the terminator. The robust way to do this is to allocate memory from the heap. But that’s slow and requires freeing the block. So you stick in a char path[MAX_PATH]. How many buffer overflow vulnerabilities has that caused? How many man-hours were lost due to poor Windows users not being able to delete deeply nested folders?

With statement expressions (gcc) we can go even farther.
Look at this fancy macro:

#define fifo_inst(T, name)                                                          \
                                                                                    \
T##FifoClass name = {                                                               \
    .self = {0},                                                                    \
    .put = ({void __fn__(T *src) {return T##FifoPut(&name.self, src);} __fn__;}),   \
    .get = ({void __fn__(T *dst) {return T##FifoGet(&name.self, dst);} __fn__;}),   \
    .len = ({ u32 __fn__(void) {return name.self.length;} __fn__;}),                \
}

With this macro (kinda self-made C++ :slight_smile: ) we can then use fifo (or whatever) like this:

int main(void) {
    inst(fifo, Message, mq1);
    // create an instance (on stack) of fifo named mq1
    ...
    mq1.put(&message[k]);
    ...
    mq1.get(&m);

It’s cool, yeah, but it’s a sort of a gimmick, while in Zig generics can be dealt with in more or less standard and clean way.

2 Likes

Just an addition. Some things are done with compiler builtins, for example
container_of@fieldParentPtr

well… take a look at

  • list.h from Linux kernel source
  • libcello - this is amazing stuff imho

Those macros may be carefully and well written, I still dislike that the programmers using c felt that this was the way they needed to take, because the language didn’t give them other ways to get generics.

For me this is geek humor, funny to look at, horrible if you encounter it unexpectedly, while trying to fix a bug.

I think if you want to have a better language, then write a better language.
Instead of adding fancy syntax to a language, thus also making it more complex, adding boilerplate, adding multiple ways of doing the same thing.

Whats missing for me there is the cleanup step of creating something where the different intents, purposes and ideas are actually thought through and integrated / balanced. The language designer brain that holds all the different aspects and weighs those, to come up with something that is designed well.
Instead of gluing two things that were well designed on their own together, I would rather use something, where somebody spent the time to come up with something that considers those together for an overall design.

I guess everyone has different ideas about what should be designed together versus apart from another and on what levels those should interact with another. For example whether something is builtin, part of the standard library, a standalone library, some external service used via some api, etc.
I think the language influences how people think about those things and what they expect. Having to invent your own generics via preprocessor in c may be a c way of doing things, but personally I want that as a language feature.

1 Like

On a sudden I have discovered for myself that we can use types in switch:

const std = @import("std");

fn MinMax(comptime T: type) type {
    return struct {

        const Self = @This();

        fn min() !T {
            return switch (T) {
                u8 => 0,
                i8 => -128,
                else => error.Unsupported,
            };
        }

        fn max() !T {
            return switch (T) {
                u8 => 255,
                i8 => 127,
                else => error.Unsupported,
            };
        }

    };
}

pub fn main() !void {
    const u8t = MinMax(u8);
    const i8t = MinMax(i8);
    std.debug.print("u8t.min = {}\n", .{try u8t.min()});
    std.debug.print("u8t.max = {}\n", .{try u8t.max()});
    std.debug.print("i8t.min = {}\n", .{try i8t.min()});
    std.debug.print("i8t.max = {}\n", .{try i8t.max()});
}