Skip to content(if available)orjump to list(if available)

Crabtime: Zig’s Comptime in Rust

Crabtime: Zig’s Comptime in Rust

212 comments

·March 19, 2025

weinzierl

I love the logo, it is brilliant.

Making Rust's macros easier is laudable. Purely from a user's perspective I find it especially annoying, that proc macros need their own crate, even if I understand the reasons for it. If I read Crabtime correctly it solves that problem, which is nice.

That being said Crabtime looks more like compile time eval on steroids to me than an analogon to Zig's comptime.

One (maybe the) distinguishing feature between comptime in Zig and Rust macros seems to me to be access to type information. In Zig you have it[1] in Rust you don't and that makes a big difference. It doesn't look like we will get that in Rust anytime soon and the projects that need it (e.g. cargo semver check) use dirty tricks (parsing RustDoc from the macro) to accomplish it. I did not see anything like that in Crabtime, but I might have missed it. At any rate, I'd expect compile time reflection for anything that claims to bring comptime to Rust.

[1] I think, but I am not a Zig expert, so please correct me if I am wrong.

pron

> One (maybe the) distinguishing feature between comptime in Zig and Rust macros seems to me to be access to type information. In Zig you have it[1] in Rust you don't and that makes a big difference.

There are other differences. First, comptime functions aren't syntactic macros. This makes them much easier to reason about and debug. You could think about them as if they were regular functions running at runtime in a partially-typed language with powerful reflection (their simplicity also means they're weaker than macros, but the point is that you can get very far with that, without taking on the difficulties associated with macros). Second, I think that comptime's uniqueness comes not from what it does in isolation, but that it makes other language features redundant, keeping the entire language simple. This means that with one simple yet just-powerful-enough feature you can do away with several other features.

The end result is that Zig is a very simple language with the expressivity of far more complicated languages. That on its own is not super unusual; in a way, JavaScript is like that, too. But Zig does it in a low-level language, and that's revolutionary. It is because of its simplicity that people compare Zig to C, but it's as expressive as C++ while also being safer than C++, let alone C.

Adding comptime to an already-complex language misses out on its greatest benefit.

GrantMoyer

Nit-pick: Javascript is not simple; I personally think it has among the most complex language semantics out of all commonly used languages.

HelloNurse

Javascript would be a simple hybrid of object oriented and functional principles if backward compatibility with old hacks and "robust" use for web page scripting didn't require a host of redundant features, syntactic bizarre special cases and and evil semantic choices: -- comments, iterating objects and arrays, the absurd equality operators and type conversions, and so on.

Lerc

Can you give an example outside of the weirdness caused by automaticy converting types? (Or 'with', which is kind-of in purgatory now)

I have considered a non backwards compatible JavaScript descendant to clean things up. It would be interesting to hear what you consider to be problems.

bungle

Simplicity in high-level: Lua, low-level: Zig (combined in one (complex): Rust?)

creata

> This makes them much easier to reason about and debug.

Can you give an example of something that's easier to reason about (e.g., an error that's easier to spot) with Zig's comptime than with macros?

> it makes other language features redundant

I'm guessing (so I might be wrong) that IDEs and users still need to be aware of the common idioms, so why does it matter whether or not those common idioms are implemented in the compiler or using comptime? (I'm not saying it doesn't matter, I'm wondering what benefits you have in mind.)

WhyNotHugo

> Can you give an example of something that's easier to reason about (e.g., an error that's easier to spot) with Zig's comptime than with macros?

Rust proc_macros takes a stream of tokens and return a stream of tokens. If your macro meant to return an instance of a specific type, it must output the correct tokens which create that instance via existing interfaces. There's some really ugly indirection in trying to understand what's going on.

This is always harder to reason about than Zig's equivalent, because in Zig you just return the thing that you want to return.

samatman

You can't reason about macros, that's not how they work.

You can read their definition, you can expand them, but there's no way to look at a macro call and reason about it, it can do anything at all. In C you don't even know what is and isn't a macro, so Rust has a modest edge in that respect.

Zig just doesn't have this problem to begin with.

dabinat

I have difficulty debugging proc macros. If I need to output some data to aid in debugging a derive macro, the only way I could think of to make that happen was to make it panic with the data as part of the message. This feels like a very clunky way to debug.

pjmlp

Zig's safety is kind of on par with Modula-2, and Object Pascal, which are equally expressive as high level languages, with low level capabilities, but apparently curly brackets have won.

The two things it adds on top, is the way nullability is handled at compile time, while the former do runtime null checks, and comptime.

baranul

> curly brackets have won

True, but at least there are curly bracket languages heavily influenced by Pascal, such as Go, V (Vlang) (has comptime too and is debatably safer), Odin, etc...

huijzer

I always on purpose avoid macros and comptime as much as possible because they usually make code much harder to reason about and to debug. Also, often macros are hard to refactor.

Would you agree with my idea, or would you say I am missing something? Does Zig aleviate some of the problems I mentioned?

vijaybritto

Now that I'm older I agree with you, but I would have been furious hearing this ten years ago :)

forks

What are some examples of other language features that comptime makes redundant?

dhruvrajvanshi

It's generic system for example, is built on top of comptime. A generic struct is just a function that takes a type as an argument and returns a struct.

``` fn Vec(comptime T: anytype) {

  return struct {

     // ...
  }
}

```

IMO having a first class generic type parameter syntax is better but this demonstrates OP's point.

pron

Generics, interfaces/traits/concepts, macros, conditional compilation, const functions/constexpr. These are four or five different features in C++ or Rust, some of which are quite complex, all expressible as one simple construct: comptime.

pjmlp

That is what I really like about the evolution of metaprogramming in C++.

While it started as a hack on how to use templates back in C++98, it has gotten quite usable nowadays in C++23, and the compile time reflection will make it even better.

All without having another language to learn about, as it happens with Rust macros, with its variations, or reliance on 3rd party crates (syn).

msk-lywenn

How is C++'s template metaprogramming not another language inside C++ today? AFAIK, the syntax and even general logic is still extremely different than regular C++

pjmlp

constexpr, consteval, if constexpr, requires, auto,... are quite regular C++.

Voultapher

[flagged]

codedokode

The problem with C++ metaprogramming is that it is pain to read and understand, unless it is your daily job.

nukem222

How on earth does zig resolve types before macros? Must be some ~~nuts~~ novel order of evaluation to get that behavior. How is this intended to function? Are there multiple layers of macros? Do you have to declare said level or is it derived? How do you use macros to define types or declare types of variables? Can you use said types in other macros?

pfg_

Zig doesn't have macros, it has functions which can be run at comptime. You can make a function that returns a type and call it from another function. All declarations are only analyzed when they are first used, and functions when called at comptime are memoized based on their arguments. The order of evaluation is really simple and predictable.

weinzierl

Just for completeness: Rust has functions which can be run at comptime as well. They are called const fn and Rust has them out of the box, no crate required. They are also true Rust and not macros with a separate syntax.

They are still not an adequate substitute for Zig's comptime feature. For one and in a sense they are much more limited than comptime functions in Zig but for another (and for better or worse) they also have much higher aspirations than Zig.

const fn must always be able to be run at compile time or run time and always produce bit-identical results. This is much harder than it looks at first glance because it must also uphold in a cross-compiling scenario where the compile time environment can be vastly different from the run time environment.

This requirement also forbids any kind of side effect, so Rust const fn are essentially pure functions and I've heard them called like that.

nukem222

> Zig doesn't have macros, it has functions which can be run at comptime

You raised my hopes and dashed them quite expertly, sir. Bravo!

wavemode

The answer is that zig doesn't have macros (i.e. syntactic transformations). Comptime functions in zig are just that - functions which run at compile time. They run after typechecking of existing types, but they are capable of creating new types. Types in Zig are just values. But they're values that don't exist at runtime.

deredede

Zig's comptime is not macros, it's staged programming / multi-stage programming.

Ygg2

Zig comptime is a C++ template done better. It also suffers from similar issues as C++ templates. You can't know function is comptime unless you put it in comptime and it passes.

See https://typesanitizer.com/blog/zig-generics.html

littlestymaar

This isn't a macro, it works as both macros and templates in C++, and regarding types it works the same way as templates in C++.

SkiFire13

I'll leave this here, try guessing what this prints:

    const std = @import("std");

    const myType = struct {
        const info = @typeInfo(myType);
        const before = info.Struct.decls.len;

        pub usingnamespace (if (before == 0) struct { pub fn baz() void {} } else struct {});

        const after1 = info.Struct.decls.len;
        const after2 = @typeInfo(myType).Struct.decls.len;
    };


    pub fn main() void {
        std.debug.print("Hello, {} {} {}!\n", .{ myType.before, myType.after1, myType.after2 });
    }

throwawaymaths

well i suppose this is a good part of the reason why usingnamespace is likely to go the way of the dodo, though if i had to guess:

hello: 4, 5, 5

nindalf

I tried the library out and it worked pretty well for me.

I had previously written a declarative macro to generate benchmark functions [1]. It worked, but I didn't enjoy the process of getting it working. Nor did I feel confident about making changes to it.

When I rewrote it using crabtime I found the experience much better. I was mostly writing Rust code now, something I was familiar with. The code is much more readable and customisable [2]. For example, instead of having to pass in the names of the modules each time I added a new one, I simply read the files from disk at compile time.

To compare the two see what the code looks like in within the braces of paste!{} in the first one and crabtime::output!{} in the second one. The main difference is that I can construct the strings using Rust code and drop them in with a simple {{ str }}. With paste!, I don't know exactly what I did, but I kept messing around until it worked.

Or compare the two loops. In the first one we have `($($year:ident {$($day:ident),+ $(,)?}),+ $(,)?)` while with crabtime we have plain Rust code - `for (year, day) in years_and_days`. I find the latter more readable.

Overall I'm quite pleased with crabtime. Earlier I'd avoid Rust metaprogramming as much as possible, but now I'd be open to writing a macro if the situation called for it.

[1] - https://github.com/nindalf/advent/blob/13ff13/benches/benche...

[2] - https://github.com/nindalf/advent/blob/b72b98/benches/benche...

stared

I love these kinds of acknowledgements, as they not only show gratitude, but also give a glimpse into the collaborative, creative process:

> We would like to extend our heartfelt gratitude to the following individuals for their valuable contributions to this project:

> timonv – For discovering and suggesting the brilliant name for this crate. Read more about it here (https://www.reddit.com/r/rust/comments/1j42fgi/comment/mg6pw...).

> Polanas – For their invaluable assistance with testing, design, and insightful feedback that greatly improved the project.

> Your support and contributions have played a vital role in making this crate better—thank you!

mplanchard

At first I was like wait this looks just like eval_macro, which I discovered a couple of weeks ago. Looks like it is just renamed! The new name is great, congrats on the improved branding :)

null

[deleted]

vlovich123

Wow this is so neat. Has anyone had any experience with it / feedback? This looks so much nicer than existing macros.

nindalf

The author announced a previous version of this crate a couple of weeks ago and this new version two days ago. So there might not be that many users.

That said I did replace a declarative macro with it. Supposedly I wrote the declarative macro according to git blame but I only have a vague idea how it works. I replaced it with crabtime and I got something that I can understand and maintain.

Overall I’d say I’m very pleased with crabtime. Previously I would have avoided Rust metaprogramming as much as possible, but now I’d feel confident to use it where appropriate.

Ygg2

It's neat, but I don't think it does the same as Zig's comptime. For one it doesn't have Zig's dynamic behavior, nor more practically compile time reflection.

Rust can't have Zig's comptime dynamic properties, same way Zig will never have Rust's compile time guarantees*.

You can't simultaneously have your dynamic cake and eat it at compile time.

* Theoretically you can have it, but it would require changing language to such extent it's not recognizable.

weinzierl

No experience. I agree that it looks nice and useful, but I don't think it is much like Zig's comptime.

norman784

This looks nice, just yesterday I was trying to make my code more concise by using some macro_rules magic, but it was a bit more than what macro_rules can handle, so I ended up just writing the whole thing. I avoid whenever I can proc macros, I wrote my fair share of macros, but I hate them, you need to add most of the time 3 new dependencies, syn, quote and proc_macros2, that adds up to the compilation times.

This looks worth the playing with and see if they can solve my issue, one thing I avoid as much as possible is to add unnecessary dependencies, didn’t check how many dependencies this will add overall to the project.

lifthrasiir

It depends on proc-macro2, syn, quote, toml and rustc_version [1]. First three are legitimately expected for any complex enough procedural macros. Toml and rustc_version are apparently for automatic Cargo configuration and fairly harmless by their own. Their transitive dependencies are also not bad: unicode-ident (from proc-macro2), serde, serde_spanned, toml_datetime (from toml), and semver (from rustc_version).

[1] https://crates.io/crates/crabtime-internal/1.1.1/dependencie...

jpgvm

This is cursed in the most wonderful way, kudos.

Sharlin

It's what the word "blursed" was coined for.

airstrike

I need to use that more often...

dymk

This looks cool, but how it impacts project compile times? They talk about how caching works for multiple invocations of the same macro with different arguments. It would be nice to have some approximate numbers for how long it takes to create, compile, and execute one of its generated projects.

codedokode

The problem with macros in Rust is that they have full access to your computer. This is literally an invitation for exploitation. I think we will see the attacks based on this vulnerability once Rust becomes more popular.

jkelleyrtp

`make myfile.mk` -> pwned

I do share the sentiment - and complain about this frequently - but any environment with build scripts can wreck your computer. Encrypt what you can, I guess, but software engineering is an extremely dangerous job wrt security.

zamalek

Its slightly more insidious: merely opening it in a text editor (assuming it has some form of lsp) could pwn you. Rust definitely isn't alone in this. Quite a few of the editors I know will run in a dumbed down mode when opening an unknown repo.

kibwen

It's even more insidious than that! Even navigating to a directory in a checkout of a hostile git repo can run arbitrary code if your shell displays git info (what branch you're on, etc).

hypeatei

Do other languages have a security model for this? I've always assumed that building arbitrary code could execute something in most languages.

I think using something like the pledge syscall from OpenBSD in the compiler could be useful. That way, it's controlled at the process level which things can be accessed on the system.

codedokode

C macros and gcc do not allow to run arbitrary code during compilation.

fulafel

The C standard is accomodating as always: https://feross.org/gcc-ownage/

kibwen

In practice every C build system already does.

cmrx64

There’s already a runtime for sandboxing macros with wasm: https://github.com/dtolnay/watt

codedokode

So you need to use hacks, like compiling code into a web-browser language and messing with config files instead of having security out-of-box?

But thank you for letting me learn something useful.

cmrx64

It’s a demonstration. wasm is a portable ISA more than a “language”. Surely it makes sense to build things incrementally, in layers? https://internals.rust-lang.org/t/pre-rfc-sandboxed-determin...

But go off, king.

null

[deleted]

treyd

Proc macros. Declarative macros do not.

null

[deleted]

the__alchemist

Deos anyone have an example beyond the one on that page? I'm having a hard time understanding.

So, I'm interested in some metaprogramming right now. I'm setting up Vec3 SIMD types, and it requires a lot of repetition to manage the various variants: f32::Vec3x8, f64::Vec3x16 etc that are all similar internally. This could be handled using traditional macros, procedural macros, or something called "code gen", which I think is string manipulation of code. Could I use crabtime to do this instead? Should I?

CGamesPlay

Honestly, this long document is probably a better link than the crates.io page: https://docs.rs/crabtime/latest/crabtime/

> This could be handled using traditional macros, procedural macros, or something called "code gen", which I think is string manipulation of code. Could I use crabtime to do this instead? Should I?

You could, it seems. Crabtime supports both the procedural macros and "code gen" approaches you are talking about.

codedokode

For simply copy-pasting code you could start with using simplest traditional macros. No matter what approach you choose your code will be pain to read and understand (maybe we need to have "show generated code" button in our IDEs).

conaclos

Actually we have a command to do exactly what you want: `expand macro`. Crabtime claims to have the same thing.