0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Zig言語用のClone-on-Writeなヘルパー型作った

Last updated at Posted at 2024-06-29

あらまし

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の相互変換

そもそも型が違うため、StringCowCStringCowの相互変換はできません。
Zigとしては、以下のような代入が可能だけども、

var s: []const u8 = undefined;
var s1 = try allocator.dupeZ(u8, "Hello World");
s = s1;

うっかりこれやって、1 Byteのメモリリーク(終端バイトが落とされる)探すのに数時間溶けたので、制約として防げて逆にハッピーかも

FAQ

1. wメソッドってビミョーじゃね?

当初は、rwだけあれば問題ないっしょって感じで仕様決めてたけど、Structフィールドへの再割り当てが必要なことに気がついた。
結果、大抵のことはinitしてassignで事足りるので、ぶっちゃけ不要。

2. mutに渡すコールバックの定義がダルいんですけど?

大抵のことはinitしてassignで事足りるので、ぶっちゃけ不要。
メソッド名が思い浮かばずに数時間溶けたので、思い入れで残してるだけ。

3. なんでinitって関数名じゃなく、initAsCopyinitAsMoveなの?

  • コピーセマンティクスにすべきところをムーブセマンティクスした場合の障害はダングリングポインタの二重破棄。
  • ムーブセマンティクスにすべきところをコピーセマンティクスした場合の障害はメモリリーク。

として顕在する。
その際、出所を検索しやすいよう意図的にこのような命名をした。

おわりに

このヘルパーを作る上で、外から保持しているスライスに触られると困るのでなんとか隠蔽したかった。
その際に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と絶望的に相性が悪いです。
ArenaAllocatordeinitで問答無用に解放されるため、別スレッド先では共有している場合、SegFaultな形で露見すると思われます。

  1. 別にinitAsCopyでも構わないが、動的に生成した文字列は責任を持って自分で解放すること

  2. assignAsCopyでも構わないが、動的に生成した文字列は責任を持って自分で解放すること

  3. 既存のスライスの内容を変更することしかできず、長さを変えたりとかはできないことに注意。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?