🫐

Zig で prettytable を書いてみた

2023/05/24に公開

はじめに

先週、私は Zig 言語を学習し、prettytable-zig という小さなプロジェクトを完成しました。以下では、開発の過程で遭遇した問題について共有したいと思います。

prettytable-zig

https://github.com/Hanaasagi/prettytable-zig

まず最初に、このプロジェクトについて紹介します。prettytable-zig は、Zig 言語で実装されたprettytable です。その目的は、ターミナル環境で表形式のデータを表示することです。以下に小さな例を示します。

const std = @import("std");
const pt = @import("prettytable");

pub fn main() !void {
    // Create table
    var table = pt.Table.init(std.heap.page_allocator);
    defer table.deinit();

    // Set table title
    try table.setTitle(&.{
        "name", "id", "favorite food",
    });

    // Add some rows
    try table.addRows(&.{
        &.{ "satoshi", "20031923", "makizushi" },
        &.{ "morikawa", "20032077", "yaki\nUdon" },
    });

    table.setAlign(pt.Alignment.center);

    _ = try table.printstd();
}

カスタムセルの色設定や、テーブルの境界線などのカスタマイズもサポートしています。詳細な情報はGitHub の README に詳細が記載されています。

開発中に遭遇した問題

Zig version:

  • zig-linux-x86_64-0.11.0-dev.3206+b9d2e0e30

build.zig の非互換性

最初、私は Zig の 0.10 バージョンを使用して開発を行っていました。しかし、いくつかのシンタックスシュガーが 0.10 バージョンでは使用できないことに気づきました。例えば、以下のようなものです。

for (0..10) |i| {
}

そのため、私は Zig のバージョンを 0.11 にアップグレードしましたが、build.zig ファイルは非互換性があり、修正が必要でした。Zig 0.11 を使用して新しい build.zig ファイルを生成することで解決できます。

var 浅いコピーが行われることがあります。

例えば

const std = @import("std");

pub fn main() !void {
    var array = std.ArrayList(u8).init(std.heap.page_allocator);
    var array2 = array;
    try array2.append(1);
    std.debug.print("{d}\n", .{array.items.len});  // output 0
    std.debug.print("{d}\n", .{array2.items.len});  // output 1
}

var array2 = array は浅いコピーが行われ、実際には別の配列を変更していることになります。正しい方法はポインタを使用することです。

    var array2 = &array;

Zig の中での「copy」の意味については、次の記事を参考にしてください:https://zig.news/gowind/beware-the-copy-32l4

*const A* A

例えば

const std = @import("std");

const A = struct {
    pub fn change(a: *A) void {
        _ = a;
    }
};

pub fn main() !void {
    var array = std.ArrayList([]A).init(std.heap.page_allocator);
    for (array.items) |row| {
        for (row) |item| {
            item.change();
        }
    }
}

コンパイル時に以下のエラーが発生します。

src/main.zig:13:17: error: expected type '*main.A', found '*const main.A'
            item.change();
            ~~~~^~~~~~~
src/main.zig:13:17: note: cast discards const qualifier
src/main.zig:4:22: note: parameter type declared here
    pub fn change(a: *A) void {

一般的に、Zig では大抵の場合、*const ポインタとして推論されますので、ここでは * を使用する必要があります。

        for (row) |*item| {
            item.change();
        }

変数のスコープ

これはおそらく Zig の現在のバグです、例えば

const std = @import("std");

const A = struct {
    pub fn name() void {}

    pub fn changeName(name: []const u8) void {}
};

pub fn main() !void {}

コンパイル時に以下のエラーが発生します。

src/main.zig:6:23: error: function parameter shadows declaration of 'name'
    pub fn changeName(name: []const u8) void {}
                      ^~~~
src/main.zig:4:9: note: declared here
    pub fn name() void {}
    ~~~~^~~~~~~~~~~~~~~~~

引数名の name と同じ名前を持つ構造体内の関数との間で名前の衝突が発生しています。このことは、コーディングをする際に非常に不便な感じがします。

時々、型の自動推論ができないことがあります

例えば

const std = @import("std");

fn t() usize {
    var i = 0;
    i += 1;
    return i;
}

pub fn main() !void {
    _ = t();
}

コンパイル時に以下のエラーが発生します

src/main.zig:4:9: error: variable of type 'comptime_int' must be const or comptime
    var i = 0;
        ^
src/main.zig:4:9: note: to modify this variable at runtime, it must be given an explicit fixed-size number type

他の言語では、変数 i は戻り値の型に基づいて usize 型と推論されることがあります。しかし、Zig では comptime の存在により、icomptime_int と推論されますが、この型は const である必要があります。現時点では、comptime のメカニズムはまだ完全ではないように感じています。

この例も、現在のコンパイラの不完全さによるものです。

const std = @import("std");

const A = struct {};

fn a(x: anytype) !usize {
    _ = x;
    return 1;
}

fn b(x: anytype) ?usize {
    _ = x;
    return 2;
}

fn dispatch(x: A, f: fn (x: anytype) usize) void {
    f(x);
}

pub fn main() !void {
    if (true) {
        dispatch(A{}, a);
    } else {
        dispatch(A{}, b);
    }
}

期待される動作は、コンパイラが関数のシグネチャが一致しないエラーを出力することですが、現時点ではコンパイラがクラッシュする可能性があります。

thread 1401477 panic: zig compiler bug: GenericPoison
Unable to dump stack trace: debug info stripped

参考になる issue は、以下のリンクです:https://github.com/ziglang/zig/issues/12373

メモリ管理は非常に複雑です

それは私自身がC言語のエンジニアではないことに関係しています。普段私が使っている言語は、メモリを自動的に管理するか、文法のルールによって制約されています。Zig を使おうとした時、私は多くのミスを犯しました。

例えば

const std = @import("std");

pub fn readFrom(reader: anytype, buf: []u8, sep: []const u8) !void {
    var row_data = std.ArrayList([]const u8).init(std.heap.page_allocator);

    while (try reader.readUntilDelimiterOrEof(buf, '\n')) |line| {
        var it = std.mem.split(u8, line, sep);
        while (it.next()) |data| {
            std.debug.print("append data: {s}\n", .{data});
            try row_data.append(data);
        }
    }

    for (row_data.items) |d| {
        std.debug.print("item {s}\n", .{d});
    }
}

pub fn main() !void {
    var data =
        \\satoshi, 1, makizushi
        \\morikawa, 2, ice
    ;

    var s = std.io.fixedBufferStream(data);
    var reader = s.reader();

    var buf: [1024]u8 = undefined;
    _ = try readFrom(reader, &buf, ",");
}

上記のコードを実行すると、次の結果が得られます。

append data: satoshi
append data:  1
append data:  makizushi
append data: morikawa
append data:  2
append data:  ice
item morikaw
item ,
item , icezushi
item morikawa
item  2
item  ice

このデータは完全に正しくありません。その原因は、std.mem.split を反復処理するたびに、元のデータである line の参照が得られるためであり、linebuf から取得されます。reader.readUntilDelimiterOrEof が呼び出されるたびに、buf の内容が上書きされるため、row_data 内のすべてのデータが更新されることになります。なぜなら、それらは同じ領域を参照しているからです。

2つ目の例はメモリリークに関するものです。

const std = @import("std");

pub fn toAnsi() ![]const u8 {
    var buffer = try std.testing.allocator.alloc(u8, 64);
    const template = "\x1b[{s};{s}m";

    var fg = "39";
    var bg = "49";

    const prefix = try std.fmt.bufPrint(buffer, template, .{ fg, bg });
    return prefix;
}

test "example " {
    var s = toAnsi() catch return;
    std.debug.print("OLD: {any}\n", .{s});

    std.testing.allocator.free(s);
}

上記の関数では、動的に連結されたASCIIエスケープ文字列を返したいと思っていますが、その長さは不定です。私は固定サイズのバッファを割り当てて、その一部領域への参照を返しています。しかし、呼び出し元にとって、彼らはその一部分しか取得できず、メモリを完全に解放することはできません。

私は他の人が書いたライブラリを参考にしました、最終的には buffer を引数として渡すようにしました。呼び出し元が提供したバッファは、メモリの割り当てと解放を担当します。

Discussion