std.Io.Writer glue crash in ReleaseSafe mode

Hi everyone! I’m encountering some issues while using the writer in the C++ bridge. Here’s the demo code:

  • build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const mod = b.addModule("demo", .{
        .root_source_file = b.path("src/glue.zig"),
        .target = target,
        .link_libc = true,
        .link_libcpp = true,
    });
    mod.addCSourceFile(.{
        .file = b.path("src/glue.cpp"),
        .flags = &.{},
        .language = .cpp,
    });
    const lib = b.addLibrary(.{
        .name = "glue",
        .root_module = mod,
    });

    const exe = b.addExecutable(.{
        .name = "demo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
            .imports = &.{},
        }),
    });

    exe.root_module.linkLibrary(lib);

    b.installArtifact(exe);

    const run_step = b.step("run", "Run the app");

    const run_cmd = b.addRunArtifact(exe);
    run_step.dependOn(&run_cmd.step);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }
}

  • src/glue.zig
const std = @import("std");
const Writer = std.Io.Writer;

comptime {
    @export(&zig__Writer__write, .{ .name = "zig__Writer__write" });
    @export(&zig__Writer__flush, .{ .name = "zig__Writer__flush" });
}

fn zig__Writer__write(w: *anyopaque, buffer: [*]const u8, size: usize) callconv(.c) void {
    const writer: *Writer = @ptrCast(@alignCast(w));
    writer.print("{s}", .{buffer[0..size]}) catch unreachable;
}

fn zig__Writer__flush(writer: *Writer) callconv(.c) void {
    writer.flush() catch unreachable;
}

  • src/glue.cpp
#include <cstddef>

extern "C" {
void zig__Writer__write(void* writer, const void* buffer, size_t size);

void zig__Writer__flush(void* writer);

void c__write(void* writer, const char *buffer, size_t size) {
  zig__Writer__write(writer, buffer, size);
  zig__Writer__flush(writer);
}
}
  • src/main.zig
const std = @import("std");
const Io = std.Io;

pub fn main(init: std.process.Init) !void {
    const c__write = @extern(*const fn (*anyopaque, [*]const u8, usize) callconv(.c) void, .{ .name = "c__write" });

    const io = init.io;
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_file_writer: Io.File.Writer = .init(.stdout(), io, &stdout_buffer);
    const stdout_writer = &stdout_file_writer.interface;

    const data_size = 1025;
    const data: [data_size]u8 = @splat('#');

    // Block 1: Failure
    {
        c__write(stdout_writer, &data, data_size);
    }

    // Block 2: Success 
    // {
    //     try stdout_writer.print("{s}", .{data});
    //     try stdout_writer.flush();
    // }
}

The above code can be compiled and run normally in debug mode, but in ReleaseSafe/ReleaseFast/ReleaseSmall modes, when data_size >= buffer_size, it crashes with the following error info:

zzzzzz@fedora ~/D/d/exp-demo> zig build run --release=safe
Segmentation fault at address 0x2f
/home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib/std/Io/File/Writer.zig:91:13: 0x10359f0 in drain (demo)
    switch (w.mode) {
            ^
/home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib/std/Io/Writer.zig:528:26: 0x10c6b63 in write (std.zig)
    return w.vtable.drain(w, &.{bytes}, 1);
                         ^
/home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib/std/Io/Writer.zig:535:51: 0x10c38da in writeAll (std.zig)
    while (index < bytes.len) index += try w.write(bytes[index..]);
                                                  ^
/home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib/std/Io/Writer.zig:1002:26: 0x10eb0ea in alignBuffer (std.zig)
        return w.writeAll(buffer);
                         ^
/home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib/std/Io/Writer.zig:1024:25: 0x10c5a27 in alignBufferOptions (std.zig)
    return w.alignBuffer(buffer, options.width orelse buffer.len, options.alignment, options.fill);
                        ^
/home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib/std/Io/Writer.zig:1140:52: 0x109a54f in printValue__anon_3725 (std.zig)
                        return w.alignBufferOptions(slice, options);
                                                   ^
/home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib/std/Io/Writer.zig:703:25: 0x109849c in print__anon_3302 (std.zig)
        try w.printValue(
                        ^
/home/zzzzzz/Documents/dev/exp-demo/src/glue.zig:11:17: 0x10981f1 in zig__Writer__write (glue.zig)
    writer.print("{s}", .{buffer[0..size]}) catch unreachable;
                ^
src/glue.cpp:9:3: 0x1098104 in c__write (/home/zzzzzz/Documents/dev/exp-demo/src/glue.cpp)
  zig__Writer__write(writer, buffer, size);
  ^
/home/zzzzzz/Documents/dev/exp-demo/src/main.zig:17:17: 0x1034b7f in main (demo)
        c__write(stdout_writer, &data, data_size);
                ^
???:?:?: 0x7f87941835b4 in __libc_start_call_main (/lib64/libc.so.6)
???:?:?: 0x7f8794183667 in __libc_start_main_alias_2 (/lib64/libc.so.6)
???:?:?: 0x1033364 in ??? (???)
run
└─ run exe demo failure
error: process terminated with signal ABRT
failed command: /home/zzzzzz/Documents/dev/exp-demo/zig-out/bin/demo

Build Summary: 5/7 steps succeeded (1 failed)
run transitive failure
└─ run exe demo failure

error: the following build command failed with exit code 1:
.zig-cache/o/240121503831e5cb19f86f2bc801725a/build /home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/zig /home/zzzzzz/.config/Code/User/globalStorage/ziglang.vscode-zig/zig/x86_64-linux-0.16.0-dev.2682+02142a54d/lib /home/zzzzzz/Documents/dev/exp-demo .zig-cache /home/zzzzzz/.cache/zig --seed 0x64a53a3a -Z04cde5472c702fe3 run --release=safe

Your glue is not compiled with the same optimization. Do note that passing zig auto layout types like this over ABI boundary has no stability guarantees whatsoever as zig does not assume that they can escape the program. What you are seeing here is different memory layout due to conflict in optimization level. On another note, you should not catch unreachable here either.

1 Like

In cpp code, only a ptr to the writer is passed, and the conversion is handled in the zig code. Why would it be affected by the layout?

Because you compiled the glue lib with different optimization mode, the Writer struct may be completely different on that side vs. your executable side. (It’s not only c++ code you are compiling into that lib, it’s also zig code)

2 Likes

What @Cloudef means is, that you should add something like this to the build.zig:

    const mod = b.addModule("demo", .{
        .root_source_file = b.path("src/glue.zig"),
        .target = target,
        .link_libc = true,
        .link_libcpp = true,
        .optimize = optimize, // <--
    });

I tried this on my machine and this makes it work.

3 Likes

Oh. I always assumed that the opt level of the lib would auto match that of the exe. Thanks.

Thank you so much! I’ll go learn more about the build system again.

1 Like

Only for zig modules. When you create a library it’s already a compiled artifact.

3 Likes

I keep wondering whether it was a good idea to factor target and optimize out of std.Build.Step.Compile into std.Build.Module back in 0.12. The rest of that refactor I can get behind, but these two options seem more fitting for whole artifacts, rather than individual modules. Personally, I would prefer them to return to Compile and have them be mandatory to fill out.

Indeed, optimize and target doesn’t do anything for modules afaik (aside from the root module).

1 Like

I’ve used it when toying around with some arm embedded stuff to have one module targeting thumb mode, another for arm mode, and then importing one from the other. I was actually surprised how well it worked.

It’d be nice if struct layout could be guaranteed to be identical in cases like that, so that you don’t need to use extern everywhere and can avoid duplicating std code. But I doubt it’s likely to happen, as I could imagine it being a pain to implement properly and has less use cases nowadays.

2 Likes

Huh! I wouldn’t have expected this to work across modules in a single compilation unit, but it totally does! I just tried with a minimal program and disassembled it to see what that looks like. Well, that changes my perspective on the matter.