openlang

Unwrapping a C pointer with .? is an assertion

.? on a pointer that came back from C is an assertion, not a check. It says null is impossible here. In a safety-checked build a null panics, and in ReleaseFast or ReleaseSmall it is undefined behaviour. For malloc that claim is false by definition.

const p = c.malloc(n).?; // asserts non-null
const q = c.malloc(n) orelse return error.OutOfMemory;

The second line returns the failure to the caller, which is the honest version. Functions returning [*c]u8 are worse, because the type has no ? to remind anyone. Coercing a C pointer to a non-optional pointer is the same assertion with no syntax at all. The Zig documentation calls a zero address there safety-checked illegal behaviour.

So in wrappers I read, every C pointer goes through an optional first, and orelse names the error. What I have not decided is which error. error.OutOfMemory is right for malloc and wrong for many functions that return null.

The conversation continues 5

Follow the reasoning
  1. Permalink

    Nullness isn't the only thing that pointer drops. The length goes too. Once the wrapper has turned the result into a [*]u8, it's the last place that knows n. I'd return p[0..n] as a slice, so the caller gets the address and the length together and bounds checks have something to work with.

    The return type also can't say who frees it, so the doc comment has to. Memory from c.malloc goes back through c.free, not through whatever allocator the caller usually passes around. If the wrapper doesn't need malloc specifically, allocating through std.heap.c_allocator keeps that pairing inside Zig's own allocator interface.

  2. Permalink

    On which error to name, the information is usually in errno, and the one rule that never changes is to read it right after the call, before anything else can run. Logging, a formatting call, really any library function is allowed to change it even when nothing failed. Python's ctypes has the same trap, which is why it makes you ask for use_errno=True and then read ctypes.get_errno() instead of the raw variable.

    So the step order I'd write down is call, check for null, copy errno into a local, then decide which error it maps to. The mapping can be a switch over a few known values with a catch-all for the rest, and that last branch is where most of the judgment goes.

  3. Permalink

    Agreed on the slice. The wrapper returns []u8 now, and its doc comment names c.free as the release, with a sentence for the error path too, where nothing is returned and nothing needs freeing.

    I'm keeping malloc itself, because the buffer is handed to a C function that may call realloc on it later. That is a transfer to C, and once it happens the slice I returned stops being valid as well, which the doc comment now says.

  4. Permalink

    Look at what that catch-all branch can return, though. A Zig error is just a name. It carries no payload, so else => error.Unexpected drops the errno value that told you what actually happened. If the value matters later, it has to travel some other way, an out parameter or a diagnostics struct the caller passes in, and the error name only says which kind of thing went wrong.

    I'd keep the known branches as small as possible, one errno to one name each, so a reader can check each case against the C man page without reading the others.

  5. Permalink

    Close to TypeScript's !, which compiles to nothing, except that Zig's .? panics in a safety-checked build. Under ReleaseFast that check is gone, so the one you leaned on in testing isn't in the build you ship.

Other sides of the same question

  1. Clearing one status flag with |= cleared all of them

    |= on a write-1-to-clear status register clears more than the bit you named. The overrun flag clears by writing 1 to it. Writ…

    Replies 5c
  2. Callbacks from C lose the arena I set up

    A C library calls back into my Odin code, and the callback can't see the context I set up before registering it. A proc "c" g…

    Replies 8odinc
  3. What the canary check looks like at -O2

    Three small C functions, gcc -O2 -fstack-protector-strong -S -masm=intel, x86-64 Linux. read_line and read_count get the same…