Zig で prettytable を書いてみた
はじめに
先週、私は Zig 言語を学習し、prettytable-zig という小さなプロジェクトを完成しました。以下では、開発の過程で遭遇した問題について共有したいと思います。
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
の存在により、i
は comptime_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
の参照が得られるためであり、line
は buf
から取得されます。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