Zig: コマンドライン引数を受け取って整数(i32)に変換する

Zig 昨日触りはじめたばかりでまだぜんぜん分かってません。


まずは std.os.argvコマンドライン引数を取得します。

pkv は確認用のユーティリティ関数で、 print key value のつもり。

// arg_to_i_v1.zig

const std = @import("std");

fn pkv(k: []const u8, v: var) !void {
    std.debug.warn("{} ({})\n", .{k, v});
}

pub fn main() !void {
    try pkv("std.os.argv", std.os.argv);
    try pkv("std.os.argv type", @TypeOf(std.os.argv) == [][*:0]u8);

    const arg1: [*:0]u8 = std.os.argv[1];
    try pkv("arg1", arg1);
}
$ zig-0.6.0 run arg_to_i_v1.zig -- 42
std.os.argv ([*:0]u8@7ffc55805dd8)
std.os.argv type (true)
arg1 (42)

コマンドライン引数が文字列(というかバイト列)の配列として取得できたので、次はこれを i32 に変換します。

標準ライブラリに std.fmt.parseInt という関数があったので適当に使ってみました。

// arg_to_i_v2.zig

const std = @import("std");

pub fn main() !void {
    const arg1: [*:0]u8 = std.os.argv[1];

    const n: i32 = try std.fmt.parseInt(
        i32,  // 変換後の型
        arg1, // 変換元の文字列
        10    // 基数
    );
}

これは型が違うためエラーになります。

./arg_to_i_v2.zig:14:5: error: expected type '[]const u8', found '[*:0]u8'
    arg1, // 変換元の文字列

ここで詰まったので、一度落ち着いて、型について見てみることにしました。


std.os.argv の型は [][*:0]u8 、 その要素(それぞれの引数)の型は [*:0]u8


[]Foo は Foo のスライス。 この表記はスライスだ、と丸呑みしてもよいですが、 これが出てきたら [] の中が空なのでコンパイル時には要素数(長さ)が決まらない(実行時に決まる) ……みたいなことを考えればいいんでしょうか。


[*]Foo は Foo の配列へのポインタ型。 うーん、この表記は覚えるしかなさそう。 Cの配列のように先頭と要素の型に関する情報は持っているが、要素数の情報は持たない。


[A:B]Foo は要素数が A で終端の要素(sentinel)が B である Foo の配列を表す型。

たとえば、[4:0]u8 は要素数が 4 で、終端の要素が 0 である u8 の配列の型、となる。

[ 11, 22, 33, 44, 0 ]
                  ^ ここ(xs[要素数])が 0

で、 [*:0]Foo は何なのか。 要素数* とは??   と思ってしまいましたが、 これは「終端が0である配列」へのポインタ型、と読めばいいようです。たぶん。 終端が 0 であることは分かっているが要素数は分からない配列の先頭を差すポインタの型。

[len:sentinel] のパターンに当てはめるのではなく、この場合は [*]Foo に終端の情報 :0 が加わっている、と捉えればよいのでしょうか。


あと残っているのは []const u8const ですね。

const を外して []u8 だけだったら何かというと、えーとこれは「u8 のスライス」ですね。

ただの []Foo だと要素を書き換えられるスライスですが、 []const Foo だと要素を書き換えられないスライスになるようです。

var xs: [3]u8 = [3]u8{ 'a', 'b', 'c' };

const slice: []u8 = xs[0..xs.len];
slice[0] = 'A';

const unmodifiable_slice: []const u8 = xs[0..xs.len];
unmodifiable_slice[0] = 'A';
//=> error: cannot assign to constant

std.fmt.parseInt[]const u8 を要求しているということは、つまり 「std.fmt.parseInt では渡されたスライスの内容を変更しません(できません)よ」 ということを引数の型で表明しているのだと思います。

std.fmt.parseInt を使う側は「呼び出した関数の中で引数で渡したものの内容が書き換えられたりしないか?」という心配をしなくて済む、ありがたい、ということでしょう。


他の例としては、たとえば OutStream の write 関数や print 関数に渡す文字列の型も []const u8 になっています。

https://github.com/ziglang/zig/blob/0.6.0/lib/std/io/out_stream.zig

pub fn write(self: Self, bytes: []const u8) Error!usize { ... }
pub fn print(self: Self, comptime format: []const u8, args: var) Error!void { ... }

ここまで分かったので、次にすべきなのは [*:0]u8 から []const u8 への変換です。

// こういう形に持っていきたい:
const slice: []const u8 = ??? // [*:0]u8 から変換する
const n: i32 = try std.fmt.parseInt(i32, slice, 10);

枝葉を無視して「配列からスライスへの変換」と考えてみます。

配列からスライスを作る基本的な方法はこう。

const xs: [3]u8 = [3]u8{ 'a', 'b', 'c' };
const slice: []u8 = xs[0..xs.len];

ですが、今必要なのは [*:0]u8 から []const u8 への変換なので、 その形に持って行きたい。

変換後は []const u8 にしたいので、とりあえず const を付けてみます。

const xs: [3]u8 = [3]u8{ 'a', 'b', 'c' };
const slice: []const u8 = xs[0..xs.len];
//             ^^^^^

えーとそれから次は……この例だと変換元が [3]u8 と要素数を指定していますが、これを [*:0]u8 にしたいので…… :0 を付けてみましょうか。

const xs: [3:0]u8 = [3:0]u8{ 'a', 'b', 'c' };
const slice: []const u8 = xs[0..xs.len];

ここから [*:0]u8 にすると、スライスに変換するときに必要な xs.len が取れなくなります。

const xs: [*:0]u8 = ...;
const slice: []const u8 = xs[0..???];

[*:0]u8 ということは、要素数は分からないけれど終端が 0 であることは分かっています。 なので、要素数を知りたければ 0 の位置を探せばいいんじゃないでしょうか。

次のような関数を書いてみました。

fn countChars(chars: [*:0]u8) usize {
    var i: usize = 0;
    while (true) {
        if (chars[i] == 0) {
            return i;
        }
        i += 1;
    }
}

これを使って、 [*:0]u8 な配列を作って []const u8 に変換するサンプルコードを書いてみました。

// arg_to_i_v3.zig 
// ... snip ...

pub fn main() !void {
    var xs: [5:0]u8 = [5:0]u8{ 'a', 'b', 'c', 0, 'd' };
    try pkv("xs", xs);
    try pkv("xs.len", xs.len);

    const xs2: [*:0]u8 = &xs;
    try pkv("xs2", xs2);
    // try pkv("xs2.len", xs2.len);
    //   => error: type '[*:0]u8' does not support field access
    const slice = xs2[0..countChars(xs2)];
    try pkv("slice", slice);
    try pkv("slice.len", slice.len);
}
$ zig-0.6.0 run arg_to_i_v3.zig 
xs (abcd)
xs.len (5)
xs2 (abc)
slice (abc)
slice.len (3)

できました。 ためしに途中の要素 xs[3] を 0 にしてみたため abc までで打ち切られていますが、 終端が 0 だということになっているのでこれはこれでよいでしょう。


ここまでできたらあとは parseInt に渡すだけです。 まとめると下記のようになりました。

// arg_to_i_v4.zig

const std = @import("std");

fn pkv(k: []const u8, v: var) !void {
    std.debug.warn("{} ({})\n", .{k, v});
}

fn countChars(chars: [*:0]u8) usize {
    var i: usize = 0;
    while (true) {
        if (chars[i] == 0) {
          return i;
        }
        i += 1;
    }
}

pub fn main() !void {
    const arg1: [*:0]u8 = std.os.argv[1];

    const arg1_unmodifiable_slice: []const u8 = arg1[0..countChars(arg1)];

    const n: i32 = try std.fmt.parseInt(
        i32,
        arg1_unmodifiable_slice,
        10
    );

    try pkv("n", n);
}
$ zig-0.6.0 run arg_to_i_v4.zig -- 42
n (42)

上記の countChars に相当する関数は標準ライブラリにあるんじゃないかと思って調べてみましたが、ちょっと分かりませんでした。

が、テストコードの中に strlen という関数があるのは見つけました。

https://github.com/ziglang/zig/blob/0.6.0/test/stage1/behavior/const_slice_child.zig#L35

fn strlen(ptr: [*]const u8) usize {
    var count: usize = 0;
    while (ptr[count] != 0) : (count += 1) {}
    return count;
}

だいたい同じことやってますね。

参考

この記事を読んだ人は(ひょっとしたら)こちらも読んでいます

zenn.dev

memo88.hatenablog.com