C apis will often use pairs of a pointer and a length to represent a slice. When the length is zero, the pointer may be null because it will not be used. Zig's definition of slices is similar. When the length of a Zig slice is zero, its pointer is considered to be undefined, and zero is a valid value of undefined (even if the type would not normally allow null).
But [*c] doesn't honor this. Slicing [*c] adds a runtime check that the pointer is not null, even if the length of the slice is zero. Which means if you have this struct on an abi boundary:
const CSlice = extern struct {
ptr: [*c]u8,
len: usize,
};
the only safe way to convert it to a slice is like this:
fn toSlice(slice: CSlice) []u8 {
return if (slice.len != 0) slice.ptr[0..slice.len] else &[0]u8{};
}
But the optimizer can't optimize this, so this has a nonzero runtime cost in release mode.
There's another way to get around it, but it gives up safety checks completely:
fn toSlice(slice: CSlice) []u8 {
// reinterpret [*c]u8 as [*]u8 without a debug null check
var maybe_undefined_nonnull_pointer: [*]u8 = undefined;
@memcpy(
@ptrCast([*]u8, &maybe_undefined_nonnull_pointer),
@ptrCast([*]const u8, &slice.ptr),
@sizeOf([*]u8),
);
return maybe_undefined_nonnull_pointer[0..slice.len];
}
A similar problem exists for C apis that use start and end pointers. Slice conversion would be start[0..end-start]. In theory this should work even if end and start are both null, because the resulting length is zero and therefore the pointer is undefined. But it doesn't because of the debug check.
The safety check for slicing [*c] should only fail if the pointer is null AND the length is nonzero. This relaxed check still doesn't allow for unchecked UB here, because the pointer is already undefined when length is zero.
?[*]T has a similar problem, you can't slice it with zero length if it's undefined and the underlying bytes happen to be zero, because the only valid syntax to do so is ptr.?[0..end], which inserts a null check at the .?. We could consider allowing slicing ?[*]T with the same check as above (pointer nonnull or length is zero). I think this would be a good change, but it's likely to be more contentious so I don't consider that a core part of this issue.
C apis will often use pairs of a pointer and a length to represent a slice. When the length is zero, the pointer may be null because it will not be used. Zig's definition of slices is similar. When the length of a Zig slice is zero, its pointer is considered to be undefined, and zero is a valid value of undefined (even if the type would not normally allow null).
But
[*c]doesn't honor this. Slicing[*c]adds a runtime check that the pointer is not null, even if the length of the slice is zero. Which means if you have this struct on an abi boundary:the only safe way to convert it to a slice is like this:
But the optimizer can't optimize this, so this has a nonzero runtime cost in release mode.
There's another way to get around it, but it gives up safety checks completely:
A similar problem exists for C apis that use start and end pointers. Slice conversion would be
start[0..end-start]. In theory this should work even if end and start are both null, because the resulting length is zero and therefore the pointer is undefined. But it doesn't because of the debug check.The safety check for slicing
[*c]should only fail if the pointer is null AND the length is nonzero. This relaxed check still doesn't allow for unchecked UB here, because the pointer is already undefined when length is zero.?[*]Thas a similar problem, you can't slice it with zero length if it's undefined and the underlying bytes happen to be zero, because the only valid syntax to do so isptr.?[0..end], which inserts a null check at the.?. We could consider allowing slicing?[*]Twith the same check as above (pointer nonnull or length is zero). I think this would be a good change, but it's likely to be more contentious so I don't consider that a core part of this issue.