Closed16

zigのメモ

shimarisu_121shimarisu_121

何か記事化するのもアレなので、スクラップ機能を利用してみます。
Zigに関するスニペット的な何かや実験などを投稿しようと思います。追加は気分でやります。
よかったら参考にしてみてください。

shimarisu_121shimarisu_121

現状Zigには正規表現がstdにないっぽいです。実装される可能性は低そう?
一応zig-regexというライブラリがあります。

代わりにCにはregex.hがあります。
これを利用して、Regex構造体なるものを考えて実装してみます。
オプション部分はpacked structpacked unionを組み合わせると、Cの伝統的な共用体フラグ取り出しみたいなのが利用できます。この場合だとCではよくフラグにREG_ICASE | REG_NOSUBとか書きますが、そんな感じのですね。これはZigのテクニックとして使えそうです。

const std = @import("std");
const re = @cImport(@cInclude("regex.h"));

pub const RegexOptions = packed struct {
    extended: bool = true, // REG_EXTENDED: 0x001
    ignore_case: bool = false, // REG_ICASE: 0x002
    newline: bool = false, // REG_NEWLINE: 0x004
    nosub: bool = false, // REG_NOSUB: 0x008
};

const RegexFlags = packed union {
    value: u4,
    flags: RegexOptions,
};

pub const Regex = struct {
    const Self = @This();
    reg: re.regex_t,
    pub fn compile(pattern: [:0]const u8, opts: RegexOptions) error{NotCompiled}!Regex {
        var reg: re.regex_t = undefined;
        const flags = RegexFlags{ .flags = opts };
        if (re.regcomp(&reg, pattern, @as(c_int, flags.value)) != 0) {
            return error.NotCompiled;
        }
        return .{ .reg = reg };
    }
    pub fn deinit(self: *Self) void {
        re.regfree(&self.reg);
    }
    pub fn match(self: *Self, text: [:0]const u8) bool {
        return re.regexec(&self.reg, text, 0, null, 0) == 0;
    }
};

使い方

var cell_phone = try Regex.compile("^0[789]0-\\d{4}-\\d{4}$", .{});
defer cell_phone.deinit();
std.debug.print("{any}\n", .{cell_phone.match("080-2121-1234")}); // true
std.debug.print("{any}\n", .{cell_phone.match("0120-3535-4200")}); // flase

var ipv4 = try Regex.compile("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$", .{});
defer ipv4.deinit();
std.debug.print("{any}\n", .{ipv4.match("192.0.0.1")}); // true
std.debug.print("{any}\n", .{ipv4.match("192.0.0.256")}); // true
std.debug.print("{any}\n", .{ipv4.match("192.0.0.2561")}); // false

var email = try Regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", .{});
defer email.deinit();
std.debug.print("{any}\n", .{email.match("example@gmail.com")}); // true
std.debug.print("{any}\n", .{email.match("example.example")}); // false
std.debug.print("{any}\n", .{email.match("example@fuga")}); // false

OK。なんかいけたっぽい。
この構造体は汎用化して使いたいことを想定しているので、ランタイム判定するようになっていますが、comptimeを使ってパターンとかを決め打ちにするような設計も面白そうですね。

ktz_aliasktz_alias

個人的には、RegexOptionsな構造に対しては、std.enums.EnumFieldStructを愛用しています。

const RegexOption = enum(u8) {extended = 1, ignore_case, newline, nosub};
const RegexOptions = std.enums.EnumFieldStruct(RegexOption, bool, false);
const RegexOptionSet = std.enums.EnumSet(RegexOption); // C-ABI連携目的

// 規定値falseなので、必要なところだけtrueに
const opts = .{
    extended = true,
    newline = true,
};

// C言語から見たら`extended | newline`な感じになる
const flags = RegexOptionSet.init(opts).bits.mask;

ご参考までに

shimarisu_121shimarisu_121

そんなのもあるんですね。
単純なビットフラグ扱うときはこれ使う方が普通に綺麗そう。ありがとうございます~

shimarisu_121shimarisu_121

CSVファイルを読み取って何かしたいときの構造体を考えてみたので、そんなスニペット。

pub fn CsvParser(comptime cols: usize) type {
    return struct {
        const Self = @This();
        const string = []const u8;

        const lf = '\n';
        const delimiter = ",";

        reader: std.io.AnyReader,

        pub fn init(reader: std.io.AnyReader) CsvParser(cols) {
            return .{ .reader = reader };
        }

        /// Read a CSV file and return a table with the specified number of columns.
        pub fn read(self: *Self, allocator: std.mem.Allocator) !CsvTable(cols) {
            var tables = std.ArrayList([cols][]const u8).init(allocator);
            var buf: [512]u8 = undefined;
            while (true) {
                const result = try self.reader.readUntilDelimiterOrEof(&buf, lf) orelse break;
                var cols_itr = std.mem.splitSequence(u8, result, delimiter);

                var row: [cols][]const u8 = undefined;
                for (0..cols) |i| {
                    const item = cols_itr.next() orelse break;
                    const value = try allocator.dupe(u8, std.mem.trim(u8, item, " "));
                    row[i] = value;
                }
                try tables.append(row);
            }
            const s = try tables.toOwnedSlice();
            return CsvTable(cols).init(allocator, s, true);
        }
    };
}

pub fn CsvTable(comptime cols: usize) type {
    return struct {
        const Self = @This();
        const string = []const u8;

        allocator: std.mem.Allocator,
        items: [][cols]string,
        hasHeader: bool,

        pub fn init(allocator: std.mem.Allocator, items: [][cols]string, hasHeader: bool) CsvTable(cols) {
            return .{ .allocator = allocator, .items = items, .hasHeader = hasHeader };
        }

        pub fn deinit(self: Self) void {
            self.allocator.free(self.items);
        }

        /// Return the number of items in the table. This is equal to the number of rows.
        pub fn len(self: Self) usize {
            return self.items.len;
        }

        /// Get the item at the specified row and column.
        pub fn get(self: Self, row: usize, col: usize) ?string {
            if (row >= self.len() or col >= cols) return null;
            return self.items[row][col];
        }

        /// Get the index of the specified column.
        pub fn indexOf(self: Self, field: []const u8) ?usize {
            if (!self.hasHeader) return null;
            const h = self.header() orelse return null;
            for (h, 0..) |f, i| {
                if (std.mem.eql(u8, f, field)) return i;
            }
            return null;
        }

        /// Return the number of columns in the table.
        pub fn colcount(_: Self) usize {
            return cols;
        }

        /// Return the number of rows in the table.
        pub fn rowcount(self: Self) usize {
            return self.len();
        }

        /// Return the header row if it exists.
        pub fn header(self: Self) ?[cols]string {
            return if (self.hasHeader and self.len() > 0) self.items[0] else null;
        }

        /// Return all rows in the table.
        pub fn rows(self: Self) [][cols]string {
            return if (self.hasHeader and self.len() > 1) self.items[1..] else self.items[0..];
        }
    };
}

処理のイメージ

const argv = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, argv);

const arg: []const u8 = if (argv.len >= 2) argv[1] else "./mock.csv";
if (!std.mem.eql(u8, std.fs.path.extension(arg), ".csv")) {
    try std.io.getStdErr().writeAll("Invalid file extension\n");
    std.posix.exit(1);
    return;
}
const file = std.fs.cwd().openFile(arg, .{ .mode = .read_only }) catch {
    try std.io.getStdErr().writeAll("Can't open file\n");
    std.posix.exit(1);
    return;
};

var parser = CsvParser(6).init(file.reader().any());
var table = try parser.read(allocator);
defer table.deinit();
shimarisu_121shimarisu_121

Zigにはforまたはwhileのループにinlineを付与することでインライン展開ができる。
が、fnキーワードにもinlineが付与できることが分かった。これを使うとCマクロ関数と同等の展開ができる。
例えば、以下の関数があるとする。

pub fn sprintf(comptime bytes: usize, comptime fmt: []const u8, args: anytype) ![]const u8 {
    var buf: [bytes]u8 = undefined;
    const result = std.fmt.bufPrint(&buf, fmt, args);
    return result;
}

それに対して、次のようなコードがあるとする。

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

    var names = std.ArrayList([]const u8).init(allocator);
    defer names.deinit();

    const srcs = [_][]const u8{
        "Alice",
        "Bob",
        "Charlie",
    };
    for (srcs) |src| {
        try names.append(try sprintf(64, "{s}", .{src}));
    }
    for (names.items) |item| {
        std.debug.print("{s}\n", .{item});
    }
}

これは次のような結果になる。

Charl
Cha
Charlie

というのも、これは固定のbuf変数のアドレスのスライスが返ってくるので、「最後に書き込まれた内容」を内部的には保持していて、「逐次で帰ってきたスライスの長さ」で返ってくるのでこんなことになっている。

これを解決する為には、関数とループの両方inlineキーワードを付与すればよい。

pub inline fn sprintf(comptime bytes: usize, comptime fmt: []const u8, args: anytype) ![]const u8 {
    var buf: [bytes]u8 = undefined;
    const result = std.fmt.bufPrint(&buf, fmt, args);
    return result;
}

inline for (srcs) |src| {
    try names.append(try sprintf(64, "{s}", .{src}));
}

これは期待通りになる。

Alice
Bob
Charlie

これは何が起こるかと言うと、appendとその中のsprintfを3回ベタに書いたのと同じような感じになる。なので別々のbufがあるような感じ。その為に期待結果通りになっている。

もっとも、恐らくinlineキーワードの過度な使用は避けたほうがよく、この場合であれば基本的にはbufを外部の引数から与えていくような関数戦略が基本的には好ましいとは思う。

shimarisu_121shimarisu_121

ArenaAllocatorを利用する構造体を作成したい時、ヒープで取るのが無難

const Buffer = struct {
    arena: *std.heap.ArenaAllocator,
    items: std.ArrayList([]const u8),

    pub fn init(allocator: std.mem.Allocator) !Buffer {
        const arena = try allocator.create(std.heap.ArenaAllocator);
        errdefer allocator.destroy(arena);
        arena.* = std.heap.ArenaAllocator.init(allocator);
        return Buffer{
            .items = std.ArrayList([]const u8).init(arena.allocator()),
            .arena = arena,
        };
    }
    pub fn deinit(self: *Buffer) void {
        const alloc = self.arena.child_allocator;
        self.arena.deinit();
        alloc.destroy(self.arena);
    }
};
shimarisu_121shimarisu_121

std.heapのやつをもうちょっと摘んでみたというもの。

std.heap.stackFallback

FixedBufferAllocatorの強化版で、原則FixedBufferAllocatorを使うが、足りない場合は別のアロケータを使うというもの。

var sfb = std.heap.stackFallback(128, gpa.allocator());
const allocator = sfb.get();

var user = try allocator.create(User);
defer allocator.destroy(user);
user.id = 1;
user.name = "Alice";

std.heap.MemoryPool

オブジェクトプーリングのAPI。パット見の解釈では、要はキャッシュ作っといて、原則それを元にファクトリとして使うみたいなやつらしい。
何かしら破棄して別の箇所で使う場合など、再活用が行われる為、同じ型の構造体などを頻繁に生成したりする場合にパフォーマンス上有益なんだと思われる。
これ自体は良いのだが、zlsの相性がいまいち現状悪く、型ヒントつけないと補完が出てこないのが難点。

var pool: std.heap.MemoryPoolExtra(User, .{}) = std.heap.MemoryPool(User).init(gpa.allocator());
defer pool.deinit();

var user = try pool.create();
user.id = 1;
user.name = "Alice";

std.debug.print("{s}\n", .{user.name});

pool.destroy(user);
shimarisu_121shimarisu_121

ちょっとやっつけだけどglob.hを使った簡易glob関数の例
ポイントと言うか学びになった点としてはstd.mem.span関数。これで[*c]u8型、いわゆるCの文字列はzigの方の型に持ってこれる。

const cglob = @cImport(@cInclude("glob.h"));

/// Searches for files with the given Glob pattern and returns a list of matching paths.
pub fn glob(allocator: std.mem.Allocator, pattern: [:0]const u8) ![][:0]const u8 {
    var result = std.ArrayList([:0]const u8).init(allocator);
    var glob_t: cglob.glob_t = undefined;
    const ret = cglob.glob(pattern, cglob.GLOB_TILDE, null, &glob_t);
    defer cglob.globfree(&glob_t);
    if (ret == 0) {
        for (0..glob_t.gl_pathc) |i| {
            const path: [:0]const u8 = std.mem.span(glob_t.gl_pathv[i]);
            try result.append(path);
        }
    }
    return try result.toOwnedSlice();
}

test "glob" {
    const allocator = std.testing.allocator;
    const result = try glob(allocator, "*.zig*");
    defer allocator.free(result);
    try std.testing.expect(result.len >= 2 and std.mem.eql(u8, result[0], "build.zig") and std.mem.eql(u8, result[1], "build.zig.zon"));
}
shimarisu_121shimarisu_121

iconv.hを使ってみるテスト。
ちょっと不安だが、期待通りではあったのでこんな感じになってくると思う。
ポインターキャストが中々シンドいね。
検証方法としてはCでまずテストコードを書いてしまい、一旦上手くいくことを確認してからzig translate-c xxxx.c -lc > xxxx.zigしてから該当の関数を引っこ抜いて補正、という感じにいくと、少し詰まるのは抑えられる。ここはzigの強みの機能なので、もう少し覚えて上手く使いたいところだね…。

const ciconv = @cImport(@cInclude("iconv.h"));

fn iconv(buf: []u8, text: [:0]const u8, to: [:0]const u8, from: [:0]const u8) ?[]u8 {
    const cd = ciconv.iconv_open(to, from) orelse return null;
    defer _ = ciconv.iconv_close(cd);

    var inptr: [*c]u8 = @as([*c]u8, @ptrCast(@alignCast(@constCast(text))));
    var inbytesleft: usize = text.len;

    var outptr: [*c]u8 = @as([*c]u8, @ptrCast(@alignCast(buf)));
    var outbytesleft: usize = @sizeOf(u8) * buf.len;

    const ret = ciconv.iconv(cd, &inptr, &inbytesleft, &outptr, &outbytesleft);
    const real_ret: i64 = @bitCast(ret);

    return if (real_ret == -1) null else buf[0..(buf.len - outbytesleft)];
}

test "iconv" {
    var buf: [64]u8 = undefined;
    const shiftjis = iconv(&buf, "こんにちは", "SHIFT_JIS", "UTF-8").?;
    try std.testing.expectEqualSlices(u8, &[_]u8{ 130, 177, 130, 241, 130, 201, 130, 191, 130, 205 }, shiftjis);
}
shimarisu_121shimarisu_121

C# -> zigで作ったDLL関数アクセス

Zigをdll化して文字列の相互やりとりをする実験。いわゆるFFI。

hello関数は文字列と文字列の長さ、出力用のポインタを渡し、zig側でアロケートしたポインタを渡す。同時に解放用の関数hello_freeを公開し、それでアンマネージド領域を解放できるものとする。

こういったものには手慣れてないのでちょっと苦労した。まず、zig側は以下のコマンドでWin用のDLLを作成できる(build.zigaddSharedLibraryを指定)。

zig build -Dtarget=x86_64-windows

以下のような共有ライブラリ関数を作る。

var gpa = std.heap.GeneralPurposeAllocator(.{}){};

var buf: [:0]u8 = undefined;

export fn hello(name: [*:0]const u8, len: u32, ret_bytes: *[*:0]const u8) i32 {
    const allocator = gpa.allocator();
    const n: []const u8 = name[0..len];
    const str: [:0]u8 = std.fmt.allocPrintZ(allocator, "hello, {s}", .{n}) catch return -1;
    errdefer allocator.free(str);
    buf = str;

    ret_bytes.* = str[0..];

    return 0;
}

export fn hello_free() void {
    defer std.debug.assert(gpa.deinit() == .ok);
    const allocator = gpa.allocator();
    allocator.free(buf);
}

.NET(C#)側は最近から公式提供され始めたLibraryImportを使う。これも扱いが手慣れてないのでちょっと苦労してるが、DllImportより恐らくこっちが推奨になってくるだろうから、使えるようにした方が良いだろうと思う。
ポイントとしてはこの関数の場合、第3引数はIntPtrで定義してしまうのがミソらしく、例えばstringだとアンマネージドなポインタはマネージドな文字列にした後、すぐさま解放してしまおう、と言うコードが生成されてしまうらしい。
こういう形にしとけば、どうやらそのあたり回避できそう。

internal partial class MyLib
{
    [LibraryImport("mylib", EntryPoint = "add")]
    public static partial int Add(int a, int b);

    [LibraryImport("mylib", EntryPoint = "hello", StringMarshalling = StringMarshalling.Utf8)]
    public static unsafe partial int Hello(string name, uint len, out IntPtr ret_bytes);

    [LibraryImport("mylib", EntryPoint = "hello_free")]
    public static partial void HelloFree();
}

internal class Program
{
    private static void Main(string[] args)
    {
        var str = "Jhon";
        var ret = MyLib.Hello(str, (uint)str.Length, out var ret_bytes);
        var text = Marshal.PtrToStringUTF8(ret_bytes);
        Console.WriteLine(text);
        MyLib.HelloFree();
    }
}

結果はこうなる。期待通りではあった。

hello, Jhon

どちらの言語もこの手のものが手慣れてないので、良いプラクティスや知識は身につけたいところ…

shimarisu_121shimarisu_121

zigで生成した静的ライブラリにヘッダーをかませて、それをリンクさせた実行ファイルを作成する方法。
ある静的ライブラリを作成して、それに関連したCヘッダーを作成するには、

zig build-lib -lc -femit-h ./src/mylib.zig

とすれば動く。なお、-lcをつけないとエラーになる。

これは間違いなく現在(v0.13.0)時点は法外で、明らかに現時点で開発者が期待している方法ではない。
恐らく「将来的につけたい機能」として保留していると思われる。
build.zig*Compile型が.getEmittedH()メソッドを持っており、これが関連すると思われるが、こちらは動かなかった。
恐らく、かなりしばらくの間は機能しないと思われる。

どうやら組み込みのzig.hは壊れており、ZIG_TARGET_MAX_INT_ALIGNMENTが定義されていない。
あくまで実験のため、この対応には生成ヘッダにて#define ZIG_TARGET_MAX_INT_ALIGNMENT (16)とか適当に入れた後zig.hをインクルードすればなんか動いた。

その後、zigでこのライブラリを(元も子もないが)リンクさせ、そのライブラリの関数をzigファイルで扱う場合、以下のようにすることで機能した。

exe.addIncludePath(.{ .cwd_relative = "xxxxxxxxxxxxxx/lib/zig" }); // zig.h
exe.addIncludePath(.{ .cwd_relative = "includes" }); // mylib.h
exe.addLibraryPath(.{ .cwd_relative = "lib" }); // libmylib.a
exe.linkLibC();
exe.linkSystemLibrary("mylib");
exe.addIncludePath(.{ .cwd_relative = "xxxxxxxxxxxxxxxxxxx/lib/zig" });
exe.addIncludePath(.{ .cwd_relative = "includes" });
exe.addObjectFile(.{ .cwd_relative = "lib/libmylib.a" });
exe.linkLibC();

対応コンパイラさえ合わせれば、CMakeやMesonのようなC系ビルドシステムでもリンクできるかもしれないが、これは未検証。

とはいえ、これは結構個人的には実現すればワクワクする機能だと感じる。
このような機能が整備されてまともに実装されれば、Zigをメイン言語としたソフトウェアにZigコンパイラを噛ませてCのコードを扱えるだけでなく、
C言語ベースのソフトウェアにZigでビルドしたライブラリを使い、ヘッダ生成した上でバインディングコードを書くことなくCから扱い、CMakeのようなビルドシステムを経由して容易にそのバイナリへ組み込めるシナリオも想定できるということになる。
Rustはbindgenのライブラリが充実しており、恐らくこういうことが出来るのだと思うが、そこといい勝負が出来るようになるだろうし、間違いなくC資産を活用した開発の幅がグッと広がる。
長い時間はかかると思うが、ぜひ将来的に実現して欲しいと思う。

shimarisu_121shimarisu_121

zig製静的ライブラリをCプログラムにリンク

Linux OS上でのC+Meson+gcc環境下でZig製Static Libraryのリンク成功。
現状は.optimizeのモードが限定される模様。オプション的に-fcompiler-rtというのが関わってくるらしいが、ヘルプで引っかからず内容がよく分からなかった。
恐らく、Zigに関わる何かしらのシンボル解決が不可能なエラーが出ていたことから、それらのシンボル情報を含めるかどうか、みたいな内容だとは想像する。

const lib = b.addStaticLibrary(.{
    .name = "mylib",
    .root_source_file = b.path("src/root.zig"),
    .target = target,
    .optimize = .ReleaseSmall,
});

Mesonの設定は以下の通り。

project('demo', 'c')

include = include_directories(
  [
    'includes',
    'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/lib/zig',
  ],
)

cc = meson.get_compiler('c')
lib = declare_dependency(
  dependencies: cc.find_library('mylib', dirs: [meson.current_source_dir() + '/lib']),
  include_directories: [include],
)

executable('demo', ['main.c'], dependencies: [lib], include_directories: [include])

いやあ素晴らしいね。出来ると期待してたけど、本当に出来ちゃうとはね!
ちょっとこの辺りは整備が本当に進んでくれば…って感じだと思うけれど、これは夢が広がるなあ!良いね!

shimarisu_121shimarisu_121

Wasm関連。完全なメモです。

  • WASI Component Model方面でzigサポートが強化されれば、Component Model向けにバインディングさせたZigコードとjco使ってjavascript・Node・ブラウザ関連との連携はやりやすくなるかもしれない
  • zigは比較的簡単な方だけど、なんだかんだWebAssemblyはめんどい…

javascriptからWASMメモリに文字列をセット

const utf8 = new TextEncoder().encode(text);
return new Uint8Array(memory.buffer, ptr, utf8.length).set(utf8);

javascriptでWASMメモリから文字列を抽出

const data = new Uint8Array(memory.buffer, ptr, length);
return new TextDecoder().decode(data);

javascriptで64バイト構造体値を分解

  • packed struct(u64)で32byte, 32byteのフィールドをjavascript上で解釈しなおす方法
const buf = new ArrayBuffer(byteLength); // byteLength = 64
new DataView(buf).setBigUint64(0, val, littleEndian);
return new Uint32Array(buf);

build.zig設定

  • wasm coreでのビルド設定は現状以下の通り
  • addExecutable()でビルドすること
  • wasiwasm32-wasiというのがある。.os_tagwasi
  • オプションには現状-fno-entry -rdynamicが必要(対応するものが以下の設定)
const exe = b.addExecutable(.{
    .name = "wasm-tool",
    .root_source_file = b.path("src/root.zig"),
    .target = b.resolveTargetQuery(.{
        .os_tag = .freestanding,
        .cpu_arch = .wasm32,
    }),
    .optimize = optimize,
});
exe.rdynamic = true;
exe.entry = .disabled;
shimarisu_121shimarisu_121

WebAssemblyでconsole.log()する方法の一例。

Zig側には以下のような関数を貼っておく

fn log(s: []const u8) void {
    jslog(@constCast(s.ptr), s.len);
}

pub extern fn jslog(ptr: [*]u8, len: u32) void;

javascript側には以下のようにenvを設定することで、wasm側が呼び出せるようになる

const {
  instance: { exports: _exports },
} = await WebAssembly.instantiate(data, {
  env: {
    jslog: (ptr: number, len: number) => {
      const mem = exports.memory as WebAssembly.Memory;
      console.log(
        new TextDecoder().decode(new Uint8Array(mem.buffer, ptr, len))
      );
    },
  },
});
shimarisu_121shimarisu_121

一定期間書き込んでないのでこのスクラップはクローズします。
また新しいことを学んだら別記事でアップしようと思います。
(コメントがある方はぜひどうぞ!)

このスクラップは9日前にクローズされました