🐿️

Zigでコマンドを探し子プロセスを叩く

2024/09/09に公開

はじめに

この記事ではZigを使ってChildProccessを叩いてみた非常に簡単な実験を書いています。
具体的には、/sbin/apk list --installedを実行する(なぜこれなのかは特に理由はありません…笑)簡単なサンプルを記載します。

私自信Zigは触れたてであり、正直標準ライブラリがどうなってるかなど言語仕様をあまり理解してません。中々情報を探しにくかったので現バージョンでの備忘録として参考になればと思います。

正直大したことはしていません。

Zigは触れてかなり虜になっています。ものすごく今後が楽しみな言語だと思います。

本題

インポート

インポート文です。zigでは標準でstdだけでなく、builtinというライブラリがあります(これ知らなくて探すのに苦労してしまった…後述するDiscordとstdのコード定義掘っていて存在に気付きました)。インポートするとGoで言うGOOSを取るみたいなことが出来るようになるっぽいです。

const std = @import("std");
const builtin = @import("builtin");

パスからコマンドを探す

環境変数PATHから指定したコマンド名のパスがあるかを探し、あればそのパスを返す関数です。
zigを触った方なら感じると思いますが、zigは伝統的なイテレータパターンの実装が多く見受けられます。なのでwhileは何気に多く使うと思われます。
ちなみにZigの特殊だと思った点の1つは、forwhileをそのままreturnに持っていけるところ。こういうのがZigっぽいやつなのかなって感じがします。

pub fn lookPath(allocator: std.mem.Allocator, cmd: []const u8) ?[]const u8 {
    const pathEnv = std.posix.getenv("PATH") orelse "";
    if (pathEnv.len == 0) return null;

    const delimiter = if (builtin.os.tag == .windows) ";" else ":";
    var paths = std.mem.split(u8, pathEnv, delimiter);
    return while (paths.next()) |path| {
        const joins = [2][]const u8{ path, cmd };
        const cmdAbs = std.fs.path.join(allocator, &joins) catch break null;
        const stat: ?std.fs.File.Stat = std.fs.cwd().statFile(cmdAbs) catch null;
        if (stat) |_| {
            break cmdAbs;
        }
    } else null;
}

子プロセス実行関数

argvに与えられたコマンドを子プロセスで実行する関数です。
余談ですが、このstd.process.Childは名前空間的にバージョン0.13.0での大きな変更の1つだったようで、現在はここにあります。それ以前とは異なるみたいなので注意してください。

pub fn execCommand(allocator: std.mem.Allocator, argv: []const []const u8) !std.process.Child.Term {
    var cp = std.process.Child.init(argv, allocator);
    return try cp.spawnAndWait();
}

メイン関数

メイン関数です。
期待結果として/sbin/apk list --installedを叩いて出力したかった(Alpineなので)のですが、出来ました。

ところでメモリ管理ですが、以下のコードにあるようにZigでは「アロケータベースでの管理」が基本となります。その内でもGeneralPurposeAllocatorArenaAllocatorの組み合わせ、Goのようなdefer構文との相性が抜群に良いです。ここが私が一番感動したところかもしれません(笑)。リーク検出と確実な解放が一発で出来るようになっています。

C/C++のような言語では「ヒープをどこで積み、そのポインタをどう補足するか」を意識してロジックを考える必要性に苛まれることが多いと思うのですが、それが激減し、「どこで解放するか」に集中できるようになっています。これが「組み込み標準」で出来るのはとても体験が良いです!

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit() == .ok);

    var arena = std.heap.ArenaAllocator.init(gpa.allocator());
    defer arena.deinit();
    const allocator = arena.allocator();

    const cmd = "apk";
    var argv = [3][]const u8{ cmd, "list", "--installed" };
    if (lookPath(allocator, cmd)) |cmdAbs| {
        argv[0] = cmdAbs;

        const stdout = std.io.getStdOut().writer();
        try stdout.print("{s} Path Found: {s}\n", .{ cmd, cmdAbs });
        try stdout.writeAll("\n");

        _ = try execCommand(allocator, &argv);
    } else {
        const stderr = std.io.getStdErr().writer();
        try stderr.print("{s} not found\n", .{cmd});
    }
}

補足: zvmを利用したDocker構成

本筋とは関係ありませんが、Alpineとzvmというバージョン管理ツールのDockerでzigを試していますので、その構成を記載しておきます。
そもそもzigはDockerみたいなのを使わなくても簡単にインストールできますが、Dockerでやってみたい方は1つ参考にしてみてください。

compose.ymlです。

services:
  app:
    container_name: app
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/app

Dockerfileです。

FROM alpine:3.20

RUN apk add --no-cache curl bash wget build-base musl-dev

RUN curl https://raw.githubusercontent.com/tristanisham/zvm/master/install.sh | bash

RUN echo "# ZVM" >> $HOME/.profile \
  && echo 'export ZVM_INSTALL="$HOME/.zvm/self"' >> $HOME/.profile \
  && echo 'export PATH="$PATH:$HOME/.zvm/bin"' >> $HOME/.profile \
  && echo 'export PATH="$PATH:$ZVM_INSTALL/"' >> $HOME/.profile \
  && source ~/.profile

ARG ZVM_CMD=/root/.zvm/self/zvm
ARG ZIG_VERSION=0.13.0

RUN ${ZVM_CMD} install ${ZIG_VERSION} --zls && ${ZVM_CMD} use ${ZIG_VERSION}

WORKDIR /app

CMD [ "/bin/ash" ]

私はVSCodeのdevContainer機能を利用しており、devContainer.jsonの構成は以下のとおりです。

{
  "name": "Existing Docker Compose (Extend)",
  "dockerComposeFile": ["../compose.yml", "docker-compose.yml"],
  "service": "app",
  "workspaceFolder": "/app",
  "customizations": {
    "vscode": {
      "extensions": [
        "ziglang.vscode-zig",
        "ms-vscode.cpptools-extension-pack",
        "ms-azuretools.vscode-docker"
      ]
    }
  }
}

おわりに

正直標準ライブラリのドキュメントもあるとはいえ、色々未整備なので情報を探すのに苦労しましたが、とりあえずここまで。
「高級言語なら一瞬でできること」なので記述量は多くなりますが、Cと比べるとこんなにやりたいことがスラスラできるなんて!って思っちゃいました。感動してます。「手堅いのに堅すぎないバランス」がとても良いですね。
まだまだ公式のパッケージマネージャが作り途中、という印象は受けましたが、ちゃんと作っていて一見は感じも悪くないことにかなり安心しました。エコシステム・ビルドシステムが充実してくれば一気に面白くなると思いますし、成熟するまで、まだかなりの時間がかかる印象を受けましたが、本当に楽しみ。
ちなみに手探りも手探りで書いてましたが、正直一番ヒントそうなインスピレーション(直接ではない)が得られたのは公式に案内されているDiscordコミュニティでした。

今後も色々試していければと思います。

Discussion