zig

zigとは、堅牢、最適、再利用可能なコンパイル言語。 C++の後継がRustと言われているが、Cの後継がzigと言われているらしい。

ちなみに、2024/05/19現在だとまだv1がリリースされてない。

公式ページのトップには以下の特徴が挙げられている。 これら1つ1つについてコード例とともに説明する。

  • シンプルでミニマリスティック
  • 強力なコンパイル時実行
  • ビルド環境構築が簡単

言語機能自体がシンプルで、汎用的で最小な機能しか持たない。

  • デストラクタなどの隠されたコードフローはなく、代わりにスコープガードを利用できる
  • すべてのメモリアロケーションはアロケータオブジェクトを通して行われるため、暗黙的にヒープ領域が確保されることがない
// ヒープに文字列をコピーする例
const std = @import("std");
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        if (.leak == gpa.deinit()) { @panic("memory leak!"); }
    }
    const allocator = gpa.allocator();
 
    const str = try allocator.dupe(u8, "helloworld");
    defer allocator.free(str);
 
    const stdout = std.io.getStdOut().writer();
    try stdout.print("{s}\n", .{str});
}

コンパイル時専用の構文が用意されているというよりは、実行時に実行されるコードを書くのと同じようにコンパイル時実行のコードを書ける。

  • コンパイル時に関数が実行できる
  • 型を値として扱える
// 固定長List型の例
const std = @import("std");
fn List(comptime T: type) type {
    return struct {
        const This = @This();
 
        allocator: *const std.mem.Allocator,
        items    : []T,
        len      : usize,
 
        pub fn init(allocator: *const std.mem.Allocator, len: usize) !This {
            return This {
                .allocator = allocator,
                .items     = try allocator.alloc(T, len),
                .len       = len,
            };
        }
        pub fn deinit(this: *This) void {
            this.allocator.free(this.items);
        }
    };
}
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer if (.leak == gpa.deinit()) { @panic("memory leak!"); };
    const allocator = gpa.allocator();
 
    var list = try List(i32).init(&allocator, 16);
    defer list.deinit();
 
    std.debug.print("{}\n", .{list.len});
}

C++の醜い山括弧から解放される!

zig関連のコマンドはビルドツール含めて全て1つの実行ファイルで完結していて、ファイルをダウンロードして展開、PATH通しをすれば環境構築が終わる。

  • C/C++のコンパイラも内蔵されてる
  • なんの設定をせずともクロスコンパイルが可能
    • コンパイル時実行時にはクロスコンパイル先環境をエミュレートしてくれる

また、ビルド設定(C++でいうCMakeLists.txt)も、以下のコード例のようにzig言語で記述することができる。 コマンドzig buildを実行するだけでビルドが実行される。

build.zig
// コアライブラリと実行可能ファイルをビルドするためのビルド設定
const std = @import("std");
 
pub fn build(b: *std.Build) void {
    // ビルドコマンドのオプションからクロスコンパイル設定を取得する
    const target = b.standardTargetOptions(.{});
 
    // ビルドコマンドのオプションから最適化に関する設定を取得する
    const optimize = b.standardOptimizeOption(.{});
 
    // coreライブラリを定義
    const core = b.addStaticLibrary(.{
        .name             = "hello",
        .root_source_file = b.path("src/core.zig"),
        .target           = target,
        .optimize         = optimize,
    });
 
    // 実行ファイルを定義
    const exe = b.addExecutable(.{
        .name             = "hello",
        .root_source_file = b.path("src/main.zig"),
        .target           = target,
        .optimize         = optimize,
    });
 
    // 実行ファイルにコアライブラリをリンクさせる
    exe.linkLibrary(core);
 
    // installタスク(makeでいうallターゲット)に実行時ファイルのビルドを追加する
    b.installArtifact(exe);
}

また全ての言語機能を使ったわけではないが、使っていて感じた良いところと悪いところを列挙する。

ドキュメントが乏しい

標準ライブラリのドキュメントは存在するが、グーグル検索にヒットすることがほとんどなく、説明もシンプルで細かい部分までは説明されていない。

OSSであるため、一応ソースコードは公開されており、zig言語自体読みやすいので、ドキュメントで問題解決しない場合はこちらを読むことになる。

ラムダ構文が存在しない

zig言語にはラムダ、無名関数、関数内関数などの構文が存在しないので、コールバックの記述が面倒くさい。 以下のように構造体のメンバ関数を使えば関数内で関数を定義することはできるが、いちいち名前をつけないといけないのでC++のラムダよりは扱いにくい。

pub fn func() void {
  const A = struct {
    pub fn func2() void { std.debug.print("Hello"); }
  };
  const func2 = A.func2;
}

ビルトイン関数の基準が謎

zig言語において@から始まる名前の関数はビルトイン関数であり、ライブラリのimportをせずに利用できるが、ビルトイン関数に選ばれる関数の基準がよくわからない。 sincosなどの数学関数までビルトイン関数となっているが、標準ライブラリが提供する形でも良かったのではないかと思う。

シンプルな言語仕様を目指すのであれば、通常関数とは異なる特別扱いになるビルトイン関数も少ない方が美しい。

言語仕様がシンプルで習得しやすい

zig言語はC言語の後継と言われるだけあって、言語仕様自体がとてもミニマルでシンプルになっている。 また、デストラクタのような隠れたコードフローが存在しないため、デバッグがしやすく、ハマりポイント自体も少ない。

公式ページでは、プログラミング言語の知識のデバッグではなくアプリケーションのデバッグに集中できると書かれていて、実際にzigを書くとこのことが実感できる。

メタプログラミングの敷居が低い

zig言語では、型を値として扱えるため、メタプログラミング専用の構文(C++の山括弧とかC++の山括弧とか)が少なく、普通のコードを書くのと同じ感じでメタプログラミングできる点が素晴らしい。

また、数少ないメタプログラミング専用の構文についても、シンプルかつ強力で、メタプログラミングのし易さについてはC++に圧勝している。 例えば複数の型の合計サイズを求める関数を書くとき、zig言語ではinline loopと呼ばれる機能を利用してシンプルに記述できるが、C++で同じことをしようとすると可変個引数と関数のオーバーロードを駆使して、さらに最後に待ち受ける膨大なコンパイルエラーを乗り越えなければならない。

get_size.zig
fn getSize(types: []const type) usize {
  var sum: usize = 0;
  inline for (types) |T| sum += @sizeOf(T);
  return sum;
}
const size = getSize(&[_]type{u8, u16, u32});
get_size.cc
template <typename T>
constexpr size_t getSize() {
  return sizeof(T);
}
template <typename T, typename U, typename... V>
constexpr size_t getSize() {
  return sizeof(T) + getSize<U, V...>();
}
constexpr auto kSize = getSize<uint8_t, uint16_t, uint32_t>();

テストを記述しやすい

zig言語にはテスト用の構文があるため、関数のすぐ近くに関数の使い方を説明するテストコードを記述できる。 コメントによるドキュメントでは実装が変更された時に内容の更新を強制できないが、テストコードであれば実装の変更とともに更新しなければならないため、この書き方ができるとコメントを書く必要がなくなってありがたい。

const std = @import("std");
 
fn fibonacci(n: u16) u16 {
    if (n == 0 or n == 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
test fibonacci {
    const x = fibonacci(10);
    try std.testing.expect(x == 55);
}

GitHubでざっと探したところ、zig言語を利用したアプリケーションとしては次のようなものが見つかった。

また、awesome-zigではzig言語向けに提供されているライブラリがまとめられている。

  • Last modified: 4 months ago