Best practice for top-level declarations when conditionally compiling

let’s assume there is comptime constant which will be used to drive conditional compilation; to simplify the issue, assume this constant is not unlike the infamous DEBUG symbol used with the C preprocessor (ie, a bool)…

i’m completely in sync with how zig if statements/expressions can leverage this comptime constant to alter what actually gets generated for runtime execution…

but what about “top-level” declarations (const, var, fn) in my source file??? presumably i can’t simply wrap these declarations in an if (like one often does with #if in C)???

let’s further assume that a relatively small percentage of my .zig source file is dependent on this comptime constant – and i’d like to keep all of the code in just ONE source file if possible…

what’s the best practice???

Example:

const builtin = @import("builtin");
const debug = (builtin.mode == std.builtin.OptimizeMode.Debug);

const myFunction = if (debug) myDebugFunction else myReleaseFunction;

In example code debug is a boolean that is true when building in Debug mode, in any Release mode it is false.
You define the final name of a function as const and you can use if or switch to select the actual function.

1 Like

but what happens (say) if myFunction should ONLY be called when debug is true??? do i have to create a myReleaseFunction that triggers an error???

likewise with a varthat is ONLY used when debug is true??? i would like to compiler to give me an error if i access a var in any other mode…

The simplest way:

const assert = std.debug.assert;

fn myFunction() void {
    assert(debug);
    ...
}

It would probably be better to check it at compile time and trigger a compile error, instead of having the error surprise you at runtime.

fn myFunction() void {
    comptime if (builtin.mode != .Debug) {
        @compileError("this function is only supported in debug mode");
    };
}

// or for regular decls
const my_const: usize = if (builtin.mode == .Debug) 1 else @compileError("...");
1 Like

this would presumably work if myFunction return a non-void value…

it also appears that i can define top-level variables or consts like this:

var foo = if (debug) @as(u32, 123);

as expected, i receive compiler errors when attempting to say foo += 1 in contexts where debug is not true… the type of foo is either u32 or void

@biosbob Help me understand the behaviour you’re trying to replicate in C-talk. I was first thinking macro, but you’re not removing a variable in this case, so I’m curious about why you’d want to change the type of foo?

If that’s the case, you’ll have to guard everywhere foo is used with if (debug) so maybe you can help explain what your use case is here (otherwise, you’ll end up with errors like void trying to find +=).

Isn’t this undefined behavior in an unsafe build mode?

long story, but i plan to build/run the SAME source file twice:

  1. in a “host” mode, in which execution takes place natively; and then
  2. in a “target” mode, in which execution takes place on some bare-metal MCU…

orchestrated by my build.zig (somehow!!!), the first pass of the program will actually compute (on the “host”) some critical config parameters which are then consumed as (comptime) constants during the second pass…

typically, a given module might have some “host-only” declarations used at runtime – albeit natively… in general, the first pass of the program can do virtually ANYTHING when it runs (eg., read/write files); the second pass of the program runs on an MCU with as little as 16K of memory…

whiie comptime is an amazing capability, i’m not convinced i can solve my system configuration requirements with just a SINGLE program pass… my “hosted” pass is essentially comptime on steriods – in that i can do all sorts of things not possible in a single pass…

but the fact that comptime even exists at all will be extremely helpful in having a single .zig source file support execution in these two very different environments…

does this make sense??? cramming a compliant BLE stack into ~10K of target memory (which i know how to do) really does requiring a different perspective on the build flow… and so far, i haven’t found any reasons why i can’t express this in zig…

I think I would use two separate modules that are imported via @import("hostmode_options") for the first and @import("target_options") for the second.
You then can attach an empty module to target_options when you are in hostmode and the other way around.
That way the module only contains declarations when it is compiled for that mode, then just have a build option to guard the code to make sure you aren’t accessing elements of target_options while your compiling the host mode variant.

If both options are partially shared, then maybe it would make more sense to use one @import("config") and switch between 2 different implementations for that module via the build system. (Instead of doing the same thing but twice, which was my first suggestion, if those configs are very different.)

The good thing about this is that you would just get compile errors for non existing declarations if you access the config from the wrong build mode.