ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
32.03k stars 2.34k forks source link

C interop: expected type `bool` found `c_int` #19950

Open Radvendii opened 1 month ago

Radvendii commented 1 month ago

Zig Version

0.13.0-dev.46+3648d7df1

Steps to Reproduce and Observed Behavior

Explanation below, but it's easier to see than explain. I've created a minimal reproduction so you can

git clone https://github.com/Radvendii/zig-scratch
cd zig-scratch
nix develop # or direnv allow
zig build

The error I get

install
└─ install zig-proj
   └─ zig build-exe zig-proj Debug native 1 errors
src/main.zig:11:12: error: expected type 'bool', found 'c_int'
    c.b = c.true;
          ~^~~~~
referenced by:
    callMain: /nix/store/2sngd0f8ary1zgqcx5r6kaf8ab4w37i1-zig-0.13.0-dev.46+3648d7df1/lib/std/start.zig:511:32
    callMainWithArgs: /nix/store/2sngd0f8ary1zgqcx5r6kaf8ab4w37i1-zig-0.13.0-dev.46+3648d7df1/lib/std/start.zig:469:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

The issue is with #include <stdbool.h> from c-code, and then referencing values from zig. bool gets mapped to zig bool, but true and false get mapped to c_ints, so a bool defined in c code can't be used as a bool when interfacing with that c code.

In particular, the generated zig code looks like

pub const @"bool" = bool;
pub const @"true" = @as(c_int, 1);
pub const @"false" = @as(c_int, 0);

Expected Behavior

It's tricky. I understand why bool would get mapped to bool, and I understand why #define false 0 in stdbool.h would get translated to a c_int. There's nothing "buggy" about either of those individually. But together they make interfacing with C code that makes use of bools kind of unworkable.

Assigning a c-defined bool to a c-defined bool-type variable seems like it should just work.

Radvendii commented 1 month ago

Thanks for the recatogrization, I wasn't sure if it was a bug or proposal.

I don't know how the tags work, so maybe this is correct anyways, but I didn't invoke zig translate-c at any point, this is just from @cImport() and @cInclude()

Radvendii commented 1 month ago

I've uncovered even weirder behaviour. In stdbool.h, we have bool defined by #define bool _Bool, so I figured if I overrode this by putting

    @cInclude("stdbool.h");
    @cUndef("bool");
    @cDefine("bool", "int");

To the top of the @cImport() statement i would force zig to define @"bool" = c_int so that the example works,

It doesn't work. Still defines @"bool" = bool. However, wherever bool is used as part of a struct, it now uses c_int.

Totally unsure what's going on now. Is mapping C bool to zig bool hard-coded? Is re-#defineing things not supported?

Radvendii commented 1 month ago

Okay, it seems indeed that this is a general thing. any time you have

const c = @cImport({
  @cDefine("foo", "1");
  @cUndef("foo");
  @CDefine("foo", "2");
});

It's going to correctly apply the second #define in any C code @cIncluded later on (replacing foo with 2), but if you access that as a variable from zig, it will go with the first definition (c.foo == 1).

I can't tell how much of this is intentional design vs. bugs. #defines don't actually have the same semantics as const declarations, but my intuition is that whatever the last #define declaration is, that one should determine how it shows up in the zig code, since that's how it would show up in C code with the same set of #includes and #defines

Pyrolistical commented 1 month ago

More minimal foo.h:

#define bool _Bool
#define true 1
#define false 0

The last 3 lines of zig translate-c foo.h:

pub const @"bool" = bool;
pub const @"true" = @as(c_int, 1);
pub const @"false" = @as(c_int, 0);
Radvendii commented 1 month ago

I have a pretty janky workaround:

@cImport({
    @cDefine("true", "(_Bool)1");
    @cDefine("false", "(_Bool)0");
    @cInclude("stdbool.h");
    @cUndef("true");
    @cUndef("false");
    @cDefine("true", "(_Bool)1");
    @cDefine("false", "(_Bool)0");

    // other @cInclude()s that use stdbool.h...
});

A couple of points of explanation:

  1. We must use (_Bool)1, not (bool)1. The latter confuses zig and it just doesn't understand what the 1 is doing there
  2. We must pre-define as well as post-define true and false. If we only define it before, stdbool.h will override, and if we only define it after, zig will use the first definition (from stdbool.h) to generate its own idea of what true and false are.
  3. We could also just replicate stdbool.h but with altered definitions of true and false if we were so inclined, it's not that long of a file.
Radvendii commented 1 month ago

It generates

pub const @"true" = @import("std").zig.c_translation.cast(bool, @as(c_int, 1));
pub const @"false" = @import("std").zig.c_translation.cast(bool, @as(c_int, 0));

Which is kind of goofy, but does work.