zigのメモ
何か記事化するのもアレなので、スクラップ機能を利用してみます。
Zigに関するスニペット的な何かや実験などを投稿しようと思います。追加は気分でやります。
よかったら参考にしてみてください。
現状Zigには正規表現がstdにないっぽいです。実装される可能性は低そう?
一応zig-regexというライブラリがあります。
代わりにCにはregex.hがあります。
これを利用して、Regex
構造体なるものを考えて実装してみます。
オプション部分はpacked struct
とpacked 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(®, 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
を使ってパターンとかを決め打ちにするような設計も面白そうですね。
個人的には、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;
ご参考までに
そんなのもあるんですね。
単純なビットフラグ扱うときはこれ使う方が普通に綺麗そう。ありがとうございます~
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();
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
を外部の引数から与えていくような関数戦略が基本的には好ましいとは思う。
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);
}
};
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);
ちょっとやっつけだけど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"));
}
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);
}
C# -> zigで作ったDLL関数アクセス
Zigをdll化して文字列の相互やりとりをする実験。いわゆるFFI。
hello
関数は文字列と文字列の長さ、出力用のポインタを渡し、zig
側でアロケートしたポインタを渡す。同時に解放用の関数hello_free
を公開し、それでアンマネージド領域を解放できるものとする。
こういったものには手慣れてないのでちょっと苦労した。まず、zig側は以下のコマンドでWin用のDLLを作成できる(build.zig
にaddSharedLibrary
を指定)。
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
どちらの言語もこの手のものが手慣れてないので、良いプラクティスや知識は身につけたいところ…
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資産を活用した開発の幅がグッと広がる。
長い時間はかかると思うが、ぜひ将来的に実現して欲しいと思う。
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])
いやあ素晴らしいね。出来ると期待してたけど、本当に出来ちゃうとはね!
ちょっとこの辺りは整備が本当に進んでくれば…って感じだと思うけれど、これは夢が広がるなあ!良いね!
Wasm関連。完全なメモです。
- WASI Component Model方面でzigサポートが強化されれば、Component Model向けにバインディングさせたZigコードとjco使ってjavascript・Node・ブラウザ関連との連携はやりやすくなるかもしれない
- zigは比較的簡単な方だけど、なんだかんだWebAssemblyはめんどい…
javascriptからWASMメモリに文字列をセット
- パフォーマンス的にはencodeInto()もアリ
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()
でビルドすること -
wasi
はwasm32-wasi
というのがある。.os_tag
がwasi
- オプションには現状
-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;
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))
);
},
},
});
一定期間書き込んでないのでこのスクラップはクローズします。
また新しいことを学んだら別記事でアップしようと思います。
(コメントがある方はぜひどうぞ!)