How to implement C macros globals functionality

Im having a general problem with how to translate Cs

#define FOO "bar"

and then being able to access this from any file

Lets imagine this file structure:

src/
|-a.zig
|-b.zig
|-utils/
| |-utils.zig

a and b are 2 different programs and the shared utilities are in utils.zig

A and B both have a common list of properties that need be defined, and if they dont define them then theres either a fallback value defined in utils or it skips parts of code (classic #ifdef)

Well, the closest i was to be able to have this is add some utils/globals.zig, with a bunch of

pub var FOO = "var";

Import it in the program files and redefine it, but first problem, they arent globals there, they need to be redefined at least inside main, and then also, even if the values are comptime known, the compiler strictly considers them runtime

A more real case:
In GNUs coreutils theres the program true, which returns a success status, and theres false, which returns a failure status. The program itself is actually written in true, and false just changes the #define EXIT_STATUS

So in true.c it uses the defined value of EXIT_STATUS for conditionally building what the program name is and what the help message says, etc. All that is comptime known, but in zig i cant find a way to get this behaviour

I feel like this question is a bit disorganized and i think thats because the macro system does so much different stuff that im a bit lost with all the questions that arise of how to translate it. Sorry in advance.

A common file with global constants is the closest thing IMHO and probably the one I would use in general. I don’t understand what you mean by “they need to be redefined at least inside main [etc]”.

I’ve always seen this mentioned as a “clever” trick. (Whenever things are “clever” in programming, be alert, they might come back to bite you.)

I don’t agree with this being particularly clever. It also needs to make the preprocessor do extra work in true.c just to make the EXIT_STATUS override also override the PROGRAM_NAME accordingly…

I don’t see how that is better than, for example:

// `truelib.zig`
const std = @import("std");

fn usage(program_name: []const u8, exit_status: u8) !u8 {
  // ...print usage with `program_name` as the program name...
  return exit_status;
}

pub fn realMain(program_name: []const u8, exit_status: u8) !void {
  if (help_or_flag_parsing_failed) {
    return usage(program_name, exit_status); 
  }
  return exit_status;
}

// `true.zig`
const truelib = @import("truelib.zig");

pub fn main() !u8 {
  return realMain("true", 0);
}

// `false.zig`
const truelib = @import("truelib.zig");

pub fn main() !u8 {
  return realMain("false", 1);
}

This is, IMHO, a more straightforward and less error prone approach.

4 Likes

Identifiers in Zig never appear out of nowhere, that’s by design. If you see x = 3, somewhere in this file you’ll se a var x = .... This makes it super easy to navigate Zig files.

Macros in C are constants. Like @jmc mentioned, if these values are constant, why do you need to redefine them? The only thing I can see is if your utils.zig requires some data to work, and it’s expecting to find this data in globals. That is actively discouraged by Zig, and it’s great that the language stopped you from doing this. With your intended design, changing a global value somewhere would change the behavior of utils.zig without you seeing the explicit relationship between the two. This is unexpected and makes utils.zig look magical.
Any data that some piece of code needs to work needs to come either internally or, if it has to come externally, you need to make it an explicit parameter. So, inside your utils.zig, you would have something like this:

// utils.zig
pub fn utilities(comptime msg: []const u8, comptime code: u8) type{
    return struct{
      pub fn exit() u8{
         std.debug.print("{s}\n", .{msg});
         return code;
       }
    };
  }

// a.zig
const utilities_file = @import("utilities/utilities.zig");
const utilities = utilities_file.utilities("Error message here", 1);

pub fn main() u8{
  return utilities.exit();
}
1 Like

Noting the suggestion to avoid “utilities” in names in the langref.

That’s easier said than done :slightly_smiling_face:

2 Likes

not really…

Temptation to use “utilities”, “miscellaneous”, or somebody’s initials is a failure to categorize, or more commonly, overcategorization. Such declarations can live at the root of a module that needs them with no namespace needed.

pub fn exit() u8 {
    std.debug.print("{s}\n", .{msg});
    return code;
}

pub fn main() u8 {
    return exit();
}

Look at how much simpler the code is when you follow the extremely straightforward advice.

3 Likes

I mean given a global pub var y in x.zig file

const x = @import("x.zig");
// cant do
x.y = false;

// need to do it in execution
fn main() void { x.y = false; }

and i completly agree with you on the solution for this true/false program. straigth forward, simple to understand… its better in every way

But in more general cases, also answering to @LucasSantos91 , c macros are indeed constant but they do can be either defined or not defined, and for that in zig i belive the only way is to define them as an optional and then redefine them when you need them. so they cant be const

This is sensible. i like the idea. But the code that im considering (im looking at gnus coreutils and gnulib and how they might be translated into zig) smetimes there are so many of this #ifdef inside functions that passing all those as parameters is cumberson and needs a smarter solution.

Since the pub var method seems unfit i ask myself what other solutions there are.

I just started considering defining a struct ProgramInfo that has the fields that it may need and then i could pass the struct around.
I have yet to try it but the questions in my head right now are if structs admit default values and if they woont have the same issue with comptime.
I fear i might be thinking in a too OOP way.

Also interested in this
Having the util functions inside the file that uses them is good, but when they are used in multiple files?
Having them in one of them doesnt feel good unless they Really clearly belong in that file in particular. So if thats not the case then isnt having a utils file reasonable?
Ofc if we can get a better name because the funcs inside do have a general theme would be better, but if not, utils seems like a good name.

(I would also create a utils file if they are for a single file but having them there makes the file too long)

https://ziglang.org/learn/build-system/#conditional-compilation

Maybe build options could support your use case here.
You could then generate several executables with different options and retrieve them in your source code via const config = @import("config");

3 Likes

Yes, that is the default solution when the number of arguments get large. There is nothing wrong with using structs at comptime. Structs can have default values, that’s easy.

As @swenninger mentioned, you could also define you utilities as a module in the build system, and pass your parameters at build time.

Right, but that’s not a constant then if it needs to be redefined. In that case IMHO you want a variable inside the top-level file struct and override its fields when instantiated, e.g.:

$ cat Thing.zig 
const std = @import("std");

overridable: []const u8 = "hello",

pub fn say(self: @This()) void {
    std.debug.print("overridable: {s}\n", .{self.overridable});
}

$ cat main.zig 
const Thing = @import("Thing.zig");

var t: Thing = .{
    .overridable = "hey there",
};

pub fn main() void {
    t.say();
}

$  zig run main.zig
overridable: hey there
1 Like

Use @import to access the public declarations of another file.

If I have a console application and a GUI application and some application utitilies, wouldn’t it be natural to create a utils.zig (or whatever) and @import that from both console and gui ? Importing gui from console might mean I need to satisfy dependencies I don’t want to have to satisfy. Potentially the same vice versa; am I too worried about this?

Surely if I can satisfy it for gui, then I can satisfy it for console as well in the same project, practically speaking; trivial build.zig rearrangement. But is then the zig compiler smart enough to not link console with the gui library dependencies because console doesn’t use UI-stuff and thus there’s no compile time known caller to gui’s dependencies?

I avoid the names ‘misc’ and ‘util’, or their expansions, and have for many years before discovering Zig. In the case, which frequently occurs, that there are functions, type definitions, and so on, which appear in many modules, I do put them in a file, and call it common.

misc and util tell me approximately nothing about what’s in the file, other than that the author wanted to stick some stuff somewhere so they could get at it later. A file called common, that tells me it has code which is going to show up in a lot of places.

But if you have a helper function in a file A, and realize you need it in file B, you can just import it there, even if B also imports A. Circular imports are handled just fine in Zig. You don’t want to make a habit out it, that can get confusing, but there’s no need to make a religion out of moving everything used twice into a separate base file which only exists to contain those little pieces.

Avoid wastebasket taxons, basically. Files are cheap, I’d rather see three files called printing, conversion, and load, than one file called misc with three unrelated functions in it.

1 Like

I ended going with the defining a struct with the info that i can pass around, have default values and optionals; and also

I had read how files are structs but this here leaves it Very clear

Marking this as the solution, but the solution is reading the thread

2 Likes