あらまし
Zig
はヒープのメモリ確保/解放を自前でやることが基本である。
文字列([]const u8
)を扱い出すと、誰が所有者なのか、静的確保されたリテラルなのか、動的確保されたものなのかを常に意識しておかないと、いともたやすくメモリエラーを起こす。
メモリエラーで落ちてくれればまだマシで、他の値が壊れたり、しばらく回してると気づかないうちにAllocator
がぶっ壊れた結果SegFault
に陥ったりすると原因究明がマジ辛い。
だもんで、防御的に文字列を複製するわけだけど、今度は所有者が曖昧になってメモリリークが頻発して悩ましい。
ってことで
文字列の参照を共有し、変更したり付け替えで参照を切り離すヘルパー型作ったった。
インストール
README
に記載したように、最初に以下のコマンドを実行して、build.zig.zon
に追加する。
zig fetch --save git+https://github.com/ritalin/zig-cow
次いで、build.zig
から依存を追加する
const dep = b.dependency("zig_cow", .{});
exe.root_module.addImport("cow", dep.module("cow"));
あとは、ソースにインクルードすることで使えるようになる
使い方
リテラルを共有する
内部でヒープアロケーションしているため、initAsCopy
関数で初期化する。
const cow = @import('cow');
// Cスタイル文字列の場合は代わりにcow.CStringCowを使う
var s = cow.StringCow.initAsCopy("Hello World");
一方、動的に生成した場合は、initAsMove
を使う1。
共有
文字列を共有する場合は、share
メソッドを呼んで渡す。
var s2 = s.share();
Struct
のフィールドの初期化や別スレッドに渡す場合がほとんどだと思う。
const Foo = struct {
s: cow.StringCow,
};
const foo: Foo = .{
.s = s.share(),
};
共有解除
参照の解除は、deinit
を呼ぶ。
どこからも参照されなくなったら自動的に消滅する。
s2.deinit();
Struct
の場合は、deinit
メソッドを用意して、その中で呼べば良い。
const Foo = struct {
pub fn deinit(self: *Foo) void {
self.s.deinit();
}
}
参照の付け替え
引数で渡されてきた対象に付け替える場合は、assignAsCopy
を呼ぶ。
このメソッドは、内部的にshare
を呼んで共有している。
また、もともと保持していたものは共有を解除する。
fn hoge(self: *Self, ss: cow.StringCow) void {
self.foo.s.assign(ss);
}
ローカルスコープで存在している対象に付け替える場合は、assignAsMove
を使う2。
fn hoge(self: *Self, allocator: std.mem.Allocator) !void {
const hw = try allocator.dupe(u8, "Hello World");
self.foo.s.assignAsMove(cow.StringCow.initAsMove(hw));
}
スライスの読み取り
r
メソッドを呼ぶことで、StringCow.Readable
インターフェースを返す。
このインターフェースのbuf
フィールド ([]const u8
)でやりとりする。
std.debug.print("{s}\n", .{foo.s.r().buf});
スライスの内容の書き換え
w
メソッドを呼ぶことでStringCow.Writable
インターフェースを返す。
このインターフェースのbuf
フィールド ([]u8
)に対して書き換えを行う3
別のスライスへの変更
mut
メソッドにスライスを返すコールバックを渡す。
コールバックの型は
// Cスタイルのスライスなら[:0]const u8を返すコールバックを用意する
fn (allocator: std.mem.Allocator, old: StringCow.Readable) []const u8;
SliceとSentinel Sliceの相互変換
そもそも型が違うため、StringCow
とCStringCow
の相互変換はできません。
Zig
としては、以下のような代入が可能だけども、
var s: []const u8 = undefined;
var s1 = try allocator.dupeZ(u8, "Hello World");
s = s1;
うっかりこれやって、1 Byteのメモリリーク(終端バイトが落とされる)探すのに数時間溶けたので、制約として防げて逆にハッピーかも
FAQ
1. w
メソッドってビミョーじゃね?
当初は、r
とw
だけあれば問題ないっしょって感じで仕様決めてたけど、Struct
フィールドへの再割り当てが必要なことに気がついた。
結果、大抵のことはinit
してassign
で事足りるので、ぶっちゃけ不要。
2. mut
に渡すコールバックの定義がダルいんですけど?
大抵のことはinit
してassign
で事足りるので、ぶっちゃけ不要。
メソッド名が思い浮かばずに数時間溶けたので、思い入れで残してるだけ。
3. なんでinit
って関数名じゃなく、initAsCopy
やinitAsMove
なの?
- コピーセマンティクスにすべきところをムーブセマンティクスした場合の障害はダングリングポインタの二重破棄。
- ムーブセマンティクスにすべきところをコピーセマンティクスした場合の障害はメモリリーク。
として顕在する。
その際、出所を検索しやすいよう意図的にこのような命名をした。
おわりに
このヘルパーを作る上で、外から保持しているスライスに触られると困るのでなんとか隠蔽したかった。
その際にOpaque
が使えるかなってことで試してみた。
Opaque
に関数実装を持たせることはできないため、ファイルスコープの関数名を定数で結びつけることで、制約を組み込むことに成功した。
const Internal = opaque {
const init = initInstance;
const clone = cloneInstance;
const write = asWritable;
const read = asReadable;
const mut = mutateBuffer;
const share = shareBuffer;
const unshare = unshareBuffer;
const counter = currentRefCounter;
};
今までOpaque
をどう使えば良いのか見出せてなかったが、一つのユースケースとして使っていけそうな気がした(気がしただけ)。
あと実際に組み込んでみて、つかえねーってなったら塩漬けにする。
追記
参照を共有するという仕様上ArenaAllocator
と絶望的に相性が悪いです。
ArenaAllocator
のdeinit
で問答無用に解放されるため、別スレッド先では共有している場合、SegFaultな形で露見すると思われます。