TL;DR
以下のIssue
にやり方書かれてるから読むよろし。
はじめに
まず自己依存モジュールとは、モジュールの内側から自身が属するモジュールをインポートすることを指している。
具体的には、x1.zig
とx2.zig
がモジュールx
に属している時
// x1.zig
const x = @import("x");
// ....
// x2.zig
const x = @import("x");
// ....
のように記述すること。
std
モジュール内で割とよく見かけると思う。
小さなモジュールであれば、各ソースユニットを相対パスでインポートすればよいが、規模が大きくなると自己依存モジュールが効果を発揮する。
ところでこの自己依存モジュール、バージョン0.10.x系では、何も考えなくても自己依存モジュールを作ることができた(らしい)。
しかし、バージョン0.11.x以降1、ちょっと意識して実装する必要がある模様。
方法については、以下のIssue
に記載されている。
この記事は、Issue
の内容を頑張って噛み砕いてみた記録。
環境
- zig 0.11系のmaster
Zig
言語がまだバージョン1.0に到達していないということもあり、将来的にちゃぶ台返されるかもしれないことに注意。
準備
ここでは説明のために、自身のパッケージの中にモジュールを作って直接利用する構成とする。2
├── build.zig
└── src
├── main.zig
├── mod_a
│ ├── a.zig
│ ├── bar.zig
│ └── foo.zig
└── mod_a.zig
それぞれの内容は、以下の通り。
- mod_a/a.zig
const mod_a = @import("mod_a");
pub fn a1() void {
mod_a.foo("foo_1");
}
pub fn a2() void {
mod_a.bar();
}
- mod_a/bar.zig
const std = @import("std");
pub fn bar() void {
std.debug.print("bar called\n", .{});
}
- mod_a/foo.zig
const std = @import("std");
pub fn foo(name: []const u8) void {
std.debug.print("{s} called\n", .{ name });
}
- mod_a.zig
pub const foo = @import("./mod_a/foo.zig").foo;
pub const bar = @import("./mod_a/bar.zig").bar;
const a = @import("./mod_a/a.zig");
pub const a1 = a.a1;
pub const a2 = a.a2;
- main.zig
const mod_a = @import("mod_a");
pub fn main() void {
mod_a.a1();
mod_a.a2();
}
実装方法
build.zig
でモジュールが利用できるよう宣言する
1. build
関数内で、std.Build.createModule
関数を使用してモジュールを定義する。
// createModule に渡す引数の型はCreateModuleOptions
// CreateModuleOptions 型は以下のような定義
//
// pub const CreateModuleOptions = struct {
// source_file: FileSource,
// dependencies: []const ModuleDependency = &.{},
// };
//
// FileSource 型は以下のようなunion型
// 通常はpathフィールドを指定することになるだろう
//
// pub const FileSource = union(enum) {
// /// A plain file path, relative to build root or absolute.
// path: []const u8,
//
// /// A file that is generated by an interface. Those files usually are
// /// not available until built by a build step.
// generated: *const GeneratedFile,
// ....
const mod_a = b.createModule(.{ .source_file = .{ .path = "./src/mod_a.zig" } });
2. 作成したモジュール自身を自身の依存リストに加える。
// dependencies の型は、std.StringArrayHashMap(*Module)
// キーに自身のモジュール内でインポートの際に指定する名前
// 値に作成したモジュールを渡す
try mod_a.dependencies.put("mod_a", mod_a);
3. CompileStep.addModule
メソッドを使用して、実行モジュールに作成したモジュールを追加する
// 第一引数は、モジュール内でインポートの際に指定する名前
// 第二引数は、作成したモジュール
exe.addModule("mod_a", mod_a);
4. try
を使うため、build
関数の戻り値を!void
にする。
全体をまとめると、build.zig
は以下のようになる。
const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) !void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const mod_a = b.createModule(.{ .source_file = .{ .path = "./src/mod_a.zig" } });
try mod_a.dependencies.put("mod_a", mod_a);
const exe = b.addExecutable(.{
.name = "self_depend_module_app",
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
exe.addModule("mod_a", mod_a);
// This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default
// step when running `zig build`).
exe.install();
// This *creates* a RunStep in the build graph, to be executed when another
// step is evaluated that depends on it. The next line below will establish
// such a dependency.
const run_cmd = exe.run();
// By making the run step depend on the install step, it will be run from the
// installation directory rather than directly from within the cache directory.
// This is not necessary, however, if the application depends on other installed
// files, this ensures they will be present and in the expected location.
run_cmd.step.dependOn(b.getInstallStep());
// This allows the user to pass arguments to the application in the build
// command itself, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| {
run_cmd.addArgs(args);
}
// テストの構成は同じなので省略
2023-6-3 追記
時期までは詳細に追いかけていないですが、少なくとも 0.11.0-dev.2969+855493bb8
のバージョンでstd.Build
のAPIが変更されています。
具体的には、exe.install()
の箇所。
0.11.0-dev.2969+855493bb8
のバージョンでは、以下のように変更する必要があります。3
b.installArtifact(exe);
注意点
以下のように、モジュール内のユニットに直接触るとコンパイルエラー(imported from module root
)になるので注意。
const mod_a = @import("mod_a");
// 直接モジュール内のユニットに触ってる
const bar = @import("./mod_a/bar.zig").bar;
pub fn main() !void {
mod_a.a1();
mod_a.a2();
bar();
}
Build Summary: 0/3 steps succeeded; 1 failed (disable with -fno-summary)
install transitive failure
└─ install self_depend_module transitive failure
└─ zig build-exe self_depend_module Debug native 1 errors
src/mod_a/bar.zig:1:1: error: file exists in multiple modules
src/main.zig:3:21: note: imported from module root
const bar = @import("./mod_a/bar.zig").bar;
^~~~~~~~~~~~~~~~~
src/mod_a.zig:2:25: note: imported from module root.mod_a
pub const bar = @import("./mod_a/bar.zig").bar;
^~~~~~~~~~~~~~~~~
-
正確には、 https://github.com/ziglang/zig/issues/14307 でモジュール管理周りが再構成されたあたり以降 ↩
-
パッケージを分け、別パッケージから利用するためには新しくなったパッケージ管理に従う必要があり説明するのが厄介なため ↩
-
元の
build.zig
のバックアップをとっておき、zig init-exe
でbuild.zig
を再作成したのち、バックアップから必要な箇所を置き換える方法でもOK ↩