A strange bus error when call c function from zig

main.zig

const std = @import("std");
const c = @cImport({
    @cInclude("impl.c");
});

pub fn main() !void {
    const src = "hello world";
    // const allocator = std.heap.page_allocator;
    // const dst = try allocator.alloc(u8, 128);
    const dst = [_]u8{0} ** 128;
    const dst_ptr = &dst;

    const ret = c.my_strlcpy(
        // @constCast(@ptrCast(dst.ptr)),
        @constCast(@ptrCast(dst_ptr)),
        @constCast(@ptrCast(src.ptr)),
        src.len + 1,
    );
    std.debug.print("result: {}\n", .{ret});
    std.debug.print("result: {s}\n", .{dst});
}

impl.c

#include <stdio.h>

size_t my_strlcpy(char *dst, const char *src, size_t dsize)
{
    const char *osrc = src;
    size_t nleft = dsize;

    if (nleft != 0) while (--nleft != 0) { /* Copy as many bytes as will fit. */
        if ((*dst++ = *src++) == '\0')
            break;
    }

    if (nleft == 0) { /* Not enough room in dst, add NUL and traverse rest of src. */
        if (dsize != 0) *dst = '\0'; /* NUL-terminate dst */
        while (*src++) ;
    }

    return(src - osrc - 1); /* count does not include NUL */
}

Compile with zig build-exe -lc main.zig -I. and execute ./main throw following errors:

Bus error at address 0x1025f256c
Panicked during a panic. Aborting.
Abort trap: 6

If I replace array’s ptr with ptr returned from allocator.alloc, then this program works, so why array’s ptr fails?

You’ve marked dst as const, which means that the data it contains is immutable. In this case, since its value is comptime-known, that allows Zig to place it in the “global read-only data” section of the binary. Trying to write to this data will trigger a bus error.

You’re explicitly bypassing the type system’s safety here with @constCast, which converts the *const [128]u8 to a *[128]u8 – you’re telling the type system “don’t worry, this data is actually mutable, you just don’t know that for some reason”. You should never use @constCast unless you are certain the underlying memory really is mutable.

The allocator version works because dst is then a pointer to heap-allocated mutable memory. The fact that this is still const dst doesn’t matter, because dst in this version is a pointer, which you aren’t trying to mutate; you’re just mutating what the pointer refers to, which is heap-allocated and thus mutable at runtime.

The correct version of your code would look like this:

const std = @import("std");
const c = @cImport({
    @cInclude("impl.c");
});

pub fn main() !void {
    const src = "hello world";
    var dst = [_]u8{0} ** 128;

    const ret = c.my_strlcpy(
        @ptrCast(&dst), // i haven't checked whether this cast is necessary but I'm guessing it is
        @ptrCast(src.ptr), // no @constCast needed here, the parameter is const anyway!
        src.len + 1,
    );
    std.debug.print("result: {}\n", .{ret});
    std.debug.print("result: {s}\n", .{dst});
}
3 Likes

Thanks for detailed answer, it works now!

This really confuse me at first. I wonder if it’s possible to declare the heap-allocated memory as immutable in Zig?

It is a bit confusing, but you’ll get the hang of it.

All of these are valid signatures:

const a: []const u8 = doThing();
var b: []const u8 = doThing();
const c: []u8 = doThing();
var d: []u8 = doThing();

// function signature:
fn doThing() []u8 { ...

The variables which are const, cannot be reassigned. Slices have two fields, .ptr and .len, if the variable is const, you can’t change the value of those fields, and you can’t reassign the whole variable to another slice.

The types which are const, point to memory which you aren’t allowed to mutate. For a and b this is illegal:

a[3] = '!';

For c and d, it’s allowed.

Potentially relevant link: Mutable and Constant Pointer Semantics

1 Like

I found const only apply to slice and pointer,

const alt_message: [5]const u8 = .{ 'h', 'e', 'l', 'l', 'o' };

This will throw following errors:

main.zig:5:27: error: pointer modifier 'const' not allowed on array child type

So this mean we can’t define a const array variable with its element mutable.

In other words, const array:[]u8 has the meaning of const array: []const u8.

1 Like

Correct, but this makes sense: an array is a value, and values are either const or var, either mutable or not so.

You can coerce an array into a slice: that creates a struct with a pointer to the head of the array, and with its length as the .len field. If the array is a variable, that slice can be either []const u8 or []u8, if the array is const, then the slice is automatically a []const u8.

So you can create a “look but don’t touch” view into a mutable array, but const alt_message: [5]const u8 would be redundant. Moreover it doesn’t make sense, because const in a type, as the error message says, makes a const pointer, and an array isn’t a pointer, of which Zig has quite a few varieties, including slices.

There have been proposals kicked around for things like a var which can be modified but not reassigned, or for fields in a struct to have const-ness even when the struct is a var, but those complicate the language more than they’re worth†. The type system we have is already fairly complex, I would say only in necessary ways which make it pleasant to work with, but it’s a lot to learn, and cutting even finer distinctions into it would multiply that.

† Since *const T and []const T are types, structs can and do have fields of those types. What you can’t do is have e.g. a u32 field which is constant even when the struct is a var. Just clarifying what I mean there.

1 Like