How to check if non-null pointer is not null?

export fn f(callback: *const fn () callconv(.c) void) callconv(.c) void {

Wait, shouldn’t this be a compiler error?

grayhatter@host ~/code $ zig build-lib file.zig
file.zig:6:16: error: parameter of type 'file.TaggedUnionType' not allowed in function with calling convention 'x86_64_sysv'
export fn func(t: TaggedUnionType) callconv(.c) void {
               ^~~~~~~~~~~~~~~~~~
file.zig:6:16: note: only extern unions and ABI sized packed unions are extern compatible
file.zig:1:25: note: union declared here
const TaggedUnionType = union(enum) {

The C API can’t express tagged unions. And equally, C can’t express non-null pointers.

edit: t: *TaggedUnionType does compile, but I’m not convinced that means it shouldn’t be a compiler error. Once *TaggedUnionType reaches the C abi, it becomes ?*TaggedUnionType, or ?*anyopaque if you prefer, or maybe *void being what C would see.

What C can express or not is somewhat philosophical, while the error is very concrete. You can’t export such a function, because it would be literally impossible for someone from outside of Zig to call it, since they wouldn’t know the union’s size alignment or internal layout. Passing a non-null pointer, on the other hand, has no such problem, anyone from any language knows what a pointer is in a given platform.

1 Like

I actually agree with you, after thinking about it a bit. This problem shouldn’t be possible because you’re right, C can give you a [*c]const fn() etc., but not a *const fn() etc., so the compiler shouldn’t accept it.

Maybe [*c] is too inconvenient and it should accept a ?*, but assuming non-nullability itself can be provided is very much against the facts.

I disagree. 1) The question is about what the behavior should be expected, that’s better described as a technical discussion, not a philosophical one. 2) there’s nothing stopping c from constructing a stack and data layout that matches the code generated by zig. Just like it’s possible for zig to conform to the c abi, it’s possible for c to conform to zig’s. The layout being Implementation defined doesn’t mean it becomes literally impossible. 3) My understanding of zig is that it won’t let you do things incorrectly. You can’t unintentionally cast one type into another type.

E.g. const thing: BasicEnum = OtherEnum.one; is a compiler error, even when BasicEnum is an exact match to OtherEnum. This is trival to fix in a few ways; But given the original export fn f(...) is a function in the C ABI, and zig has an ABI that happens to be an exact match, it should still be an explicit conversion. And then once it’s required to be explicit, IMO it’s better to restrict the allowed types to types that can actually be expressed by the used calling conventions. in this case the callback: *const fn () callconv(.c) void instead of something like * align(1) u64 the callback would be more accurately described/typed as an imagined * abiConvention(.c) const fn () callconv(.c) void because the pointer can’t be a zig pointer. But then, that’s your argument

because it would be literally impossible for someone from outside of Zig to call it, since they wouldn’t know the union’s size alignment or internal layout

just phrased differently.

Well, at the moment, what stops C from adhering to Zig’s ABI is that Zig doesn’t disclose it’s ABI, even to us, its users. And that’s on purpose, as it wants the freedom to change it at any time. In order for C to use Zig’s ABI, while still preserving the liberties that Zig wants, Zig would need to emit the ABI that it’s using for a particular struct, and the C compiler would have to interpret that ABI and go along with it. Zig could do its part of this deal, but I don’t think C compilers care enough about Zig in order to do their part.

They were just saying that it’s possible, not practical. One can figure out the ABI, and make a call from C use it, without it being documented or stable.

4 Likes

I took @grayhatter’s point to be that Zig won’t let us define a fn (*foo) and then pass it a ?*foo, but it does let us define a fn (*foo) @callconv(.c) and pass that a ?*foo, or really a [*c]foo to be pedantic about it, because those are things C can practically provide, and a *foo is not.

I think it’s a decent point. Someone else could make a case for the ergonomics of the thing, but I don’t think it’s all that strong.

I don’t care much, in the grand scheme of things. But the point itself is well-taken.

The current behavior is:

Coerces to other pointer types, as well as Optional Pointers. When a C pointer is coerced to a non-optional pointer, safety-checked Illegal Behavior occurs if the address is 0.

So it’s a deliberate choice, a documented one, and defensible, as it has no unsafe consequences worse than use of .? would cause.

2 Likes

The problem is that zig does not accept multiple or nullable C function pointers; Why?

error: function pointers must be single pointers
export fn f(callback: [*c]const fn () callconv(.c) void) callconv(.c) void {
                                ^~~~~~~~~~~~~~~~~~~~~~~
1 Like

Multiple function pointers do not exist because: What is callback[1]? What is callback[i] with i being a runtime var? Since functions bodies have no fixed size, there cannot exist a array of them.

C declaration example:

double (*operations[4])(double, double) = { add, subtract, multiply, divide };

operations is multiple function pointer.

EDIT:
You are right. I can see now that this multiple function pointer is actually [*c]*const fn ()

But the nullablity problem of *const fn remains.

1 Like

This is just an assertion with safety enabled, except worse because now it’s hard-coded rather than a module setting.

  1. it’s almost always wrong to use @intFromPtr
  2. this should clearly be an implicit safety check always added by the compiler in safe modes for non-optional pointers. alignment too.
2 Likes

Yeah, this would solve my immediate problem.

I also convinced myself that, in addition to all extern function return values, and exported function arguments, I do want an explicit built-in to request those checks manually, because Zig compiler doesn’t know the exhaustive list of boundary points. Some examples:

  • An export function can accept ptr_ptr: [*c]*u8, ptr_ptr_count: usize and its the user-code that materializes the slice.
  • In GC+JIT runtime for a programming language, GC would like to safety-check Zig data structures, which could easily get corrupted by bugs in JIT.
  • After casting []const u8 to *const T, compiler might not be able to do “deep” checks, if T something like Table(K, V) which stores u8 internally, but assumes that they are Ks and Vs

EDIT: filed #30729 - Add safety checks for return values of extern functions and parameters of exported functions. - ziglang/zig - Codeberg.org to make sure that both return values of extern and parameters of export functions are captured.

???

Yeah no that’s a good reason to forbid it actually. C of course allows it, but that’s no excuse for Zig to allow a particular evil sort of undefined behavior to sneak behind the cordon sanitaire.

Answers all of my remaining questions.

For those not already familiar with SAL, I’ll leave this here…

Using SAL Annotations to Reduce C/C++ Code Defects | Microsoft Learn

SAL is a system for annotating C/C++ function parameters to extend the C/C++ type systems to make stronger API guarantees such as non-nullability and relations between parameters such as pointers and lengths and terminators. It would allow for expressing the equivalent of a slice.

There was a period of time where a significant part of my job was retrofitting SAL annotations to existing codebases. Other similar systems exist.

SAL was perhaps the precursor of what became the Windows API metadata efforts (WinMD).
Microsoft generate Rust bindings for Windows from WinMD, as does Marler’s ZigWin32 project.

These are all connected ideas for doing better FFI.