🐡

Zigからlibcのライブラリを呼び出してUNIX時間を文字列表現に変換する

2022/12/25に公開

本記事はZigアドベントカレンダー25日目の記事です。

前回の記事の続きです。
UNIX時間をローカルタイムの文字列に変換するのに、現在(version 0.10.0)のZigの標準ライブラリでは機能が足りないようなので、libcのライブラリの関数を呼び出してみることにしました。

ctime_r を使う例

特に書式にこだわらないのならばctime_r()を1個使えば済みます。

ctime.zig
const std = @import("std");
const log = std.log;
const mem = std.mem;
const time = std.time;
const c = @cImport(@cInclude("time.h"));

fn c_ctime(t: i64, buf: []u8) usize {
    const ct = @intCast(c.time_t, t);
    const cbuf = @ptrCast([*c]u8, buf.ptr);
    if (c.ctime_r(&ct, cbuf) != cbuf) {
        return 0;
    }
    return mem.sliceTo(buf, 0).len;
}

pub fn main() !void {
    var buf: [32]u8 = .{};
    const t = time.timestamp();
    const n = c_ctime(t, &buf);
    log.info("len={d}, buf=[{s}]", .{n, buf[0..n]});
}

const expect = std.testing.expect;
const eql = std.mem.eql;
const debug = std.debug;

test "test ctime" {
    debug.print("\n", .{});
    var buf: [32]u8 = undefined;
    
    const t0: i64 = 0;
    const n0 = c_ctime(t0, &buf);
    debug.print("len={d}, buf=[{s}]\n", .{n0, buf[0..n0]});
    try expect(eql(u8, buf[0..n0], "Thu Jan  1 09:00:00 1970\n"));

    const t1: i64 = 1671795946;
    const n1 = c_ctime(t1, &buf);
    debug.print("len={d}, buf=[{s}]\n", .{n1, buf[0..n1]});
    try expect(eql(u8, buf[0..n1], "Fri Dec 23 20:45:46 2022\n"));
}    

テストの実行結果。(念の為環境変数でタイムゾーンをJSTに明示的に指定しています)

$ TZ=JST-9 zig test -lc ctime.zig
Test [1/1] test.test ctime... 
len=25, buf=[Thu Jan  1 09:00:00 1970
]
len=25, buf=[Fri Dec 23 20:45:46 2022
]
All 1 tests passed.

strftime を使う例

書式の指定をするならば、localtime_r()strftime()を組み合わせて使います。
strftimeの書式の指定方法はmanを参照してください。
https://man7.org/linux/man-pages/man3/strftime.3.html

time2str.zig
const std = @import("std");
const time = std.time;
const log = std.log;
const c = @cImport(@cInclude("time.h"));

pub fn time2str(t: i64, buf: []u8, fmt: [*c]const u8) usize {
    const ct = @intCast(c.time_t, t);
    const cbuf = @ptrCast([*c]u8, buf.ptr);
    var tm0: c.struct_tm = undefined;
    if (c.localtime_r(&ct, &tm0) != &tm0) {
        return 0;
    }
    return c.strftime(cbuf, buf.len, fmt, &tm0);
}

pub fn main() !void {
    var buf: [1024]u8 = undefined;
    const t = time.timestamp();
    const fmt: [*c]const u8 = "%F %T%z";
    const n = time2str(t, &buf, fmt);
    log.info("{s}", .{buf[0..n]});
}

const expect = std.testing.expect;
const eql = std.mem.eql;
const debug = std.debug;

test "test time2str" {
    var buf: [1024]u8 = undefined;
    var n:usize = undefined;
    debug.print("\ntest time2str\n", .{});
    
    const t0: i64 = 0;
    n = time2str(t0, &buf, "%F %T%z");
    debug.print("len={d}, buf=[{s}]\n", .{n, buf[0..n]});
    try expect(eql(u8, buf[0..n], "1970-01-01 09:00:00+0900"));
    
    n = time2str(t0, &buf, "%c %Z");
    debug.print("len={d}, buf=[{s}]\n", .{n, buf[0..n]});
    try expect(eql(u8, buf[0..n], "Thu Jan  1 09:00:00 1970 JST"));

    const t1: i64 = 1671795946;
    n = time2str(t1, &buf, "%F %T%z");
    debug.print("len={d}, buf=[{s}]\n", .{n, buf[0..n]});
    try expect(eql(u8, buf[0..n], "2022-12-23 20:45:46+0900"));
    
    n = time2str(t1, &buf, "%c %Z");
    debug.print("len={d}, buf=[{s}]\n", .{n, buf[0..n]});
    try expect(eql(u8, buf[0..n], "Fri Dec 23 20:45:46 2022 JST"));
}    

テストの実行結果。

$ TZ=JST-9 zig test -lc time2str.zig
Test [1/1] test.test time2str... 
test time2str
len=24, buf=[1970-01-01 09:00:00+0900]
len=28, buf=[Thu Jan  1 09:00:00 1970 JST]
len=24, buf=[2022-12-23 20:45:46+0900]
len=28, buf=[Fri Dec 23 20:45:46 2022 JST]
All 1 tests passed.

ZigからCの関数を呼び出すときのコツ

上記のプログラムはここまで書くのにけっこう苦労しました。試行錯誤してたどり着いた方法を共有します。
ポイントはこんな感じです。

  • zig translate_c を使ってヘッダファイルをZigに変換し、使いたい関数の宣言を確認する
  • 引数、戻り値をキャストして合わせ込む
  • 文字列の変換
    • 文字列リテラル
    • Zigの文字列からCの文字列へ
    • Cの文字列からZigの文字列へ

zig translate_c を使ってヘッダファイルをZigに変換し、使いたい関数の宣言を確認する

t0.c
#include <time.h>
t0.sh
ZIGDIR=/opt/zig-linux-x86_64-0.10.0

zig translate-c \
-I $ZIGDIR/lib/libc/include/x86_64-linux-gnu \
-I $ZIGDIR/lib/libc/include/generic-glibc \
-I $ZIGDIR/lib/libc/include/any-linux-any \
t0.c > t0.zig
$ wc -l t0.zig
846 t0.zig

800行以上あります。
この中で使用したい関数の型がどのように定義されているかを確認します。

$ grep ' ctime_r' t0.zig
pub extern fn ctime_r(noalias __timer: [*c]const time_t, noalias __buf: [*c]u8) [*c]u8;

[*c]Tは 特殊なポインタ型でCのポインタを表しています。

引数、戻り値をキャストして合わせ込む

@intCast@ptrCast でZigの変数からキャストして合わせ込みます。
うまくいかないとコンパイルエラーになります。
よくわからない場合には、使いたい関数を使う小さなサンプルプログラムをCで書いて、それをzig translate-cでZigに変換して参考にします。

time2str.c
#include <stdio.h>
#include <time.h>

size_t time2str(time_t t, char *buf, size_t buflen, const char *fmt)
{
	struct tm tm0;
	if (localtime_r(&t, &tm0) == (struct tm*)0) {
		return 0;
	}
	return strftime(buf, buflen, fmt, &tm0);
}

int main()
{
	char buf[1024];
	time_t t = time(0);
	const char *fmt = "%F %T%z";
	size_t n = time2str(t, buf, sizeof(buf), fmt);
	if (n != 0) {
		puts(buf);
	}
}

これをzig tranlate-cで変換するとこうなりました。

t.c
    ...
pub export fn time2str(arg_t: time_t, arg_buf: [*c]u8, arg_buflen: usize, arg_fmt: [*c]const u8) usize {
    var t = arg_t;
    var buf = arg_buf;
    var buflen = arg_buflen;
    var fmt = arg_fmt;
    var tm0: struct_tm = undefined;
    if (localtime_r(&t, &tm0) == @intToPtr([*c]struct_tm, @as(c_int, 0))) {
        return 0;
    }
    return strftime(buf, buflen, fmt, &tm0);
}
pub export fn main() c_int {
    var buf: [1024]u8 = undefined;
    var t: time_t = time(null);
    var fmt: [*c]const u8 = "%F %T%z";
    var n: usize = time2str(t, @ptrCast([*c]u8, @alignCast(@import("std").meta.alignment([*c]u8), &buf)), @sizeOf([1024]u8), fmt);
    if (n != @bitCast(c_ulong, @as(c_long, @as(c_int, 0)))) {
        _ = puts(@ptrCast([*c]u8, @alignCast(@import("std").meta.alignment([*c]u8), &buf)));
    }
    return 0;
}
    ...

文字列の変換

文字列の扱い方がZigとCでは全く異なるため、これの相互変換が必要になります。
Zigでは文字列は特別なものでなく単なる[]u8 (u8のスライス)という扱いです。バイト数の情報を保持していてNUL文字で終端されません。
Cの文字列はNUL文字で終端されたchar * (charへのポインタ)です。バイト数の情報を保持していません。
(** 2022/0106 内容をアップデートしました **)

文字列リテラル

Zigの文字列リテラルは暗黙のうちにNUL終端してくれます
そのためこのように書かれた文字列リテラルはそのままCに持っていくことができます。
const fmt: [*c]const u8 = "message";

Zigの文字列からCの文字列へ

文字列リテラルでなく、その部分文字列から作られた[]u8はNUL終端されていないので、NUL文字を追加する必要があります。バッファに空きがあればその部分にNULを書き込みます。そうでない場合には新たに領域を確保する必要があります。
std.cstr.addNullByte()というそのための関数が用意されています。これはヒープを使うので、アロケータの指定と使用後の解放が必要になります。
(** 2024/04/06 修正。std.cstr.addNullByte()はzig 0.11ではdeprecatedになっていて、代わりにstd.mem.Allocator.dupeZを使えということでした。 以下のサンプルコードも修正しています。)

Cの文字列からZigの文字列へ

Cの文字列のバイト数がわかっている場合にはそこから[]u8を作ります。
もしもわからない場合でもstd.mem.sliceTo()という関数を使えばバイト数を数えてスライスを作ってくれます。

文字列の相互変換のテスト

string_test.zig
const std = @import("std");
const expect = std.testing.expect;
const eql = std.mem.eql;
const debug = std.debug;
const c = @cImport(@cInclude("stdio.h"));

test "string conversion" {
    var buf: [1024]u8 = undefined;
    const hello: [*c]const u8 = "Hello";
    const world: []const u8 = "world wide web"[0..5];
    //const c_world = try std.cstr.addNullByte(std.testing.allocator, world);
    const c_world = try std.mem.Allocator.dupeZ(std.testing.allocator, u8, world);
    defer std.testing.allocator.free(c_world);
    const c_n = c.snprintf(@as([*c]u8, @ptrCast(&buf)), buf.len, "%s, %s.", hello, @as([*c]const u8, @ptrCast(c_world)));
    const n = @as(usize, @intCast(c_n));
    //  Cの文字列の長さがわかっている場合は、それでスライスを作る
    debug.print("\nn={d} output=[{s}]\n", .{ n, buf[0..n] });
    try expect(eql(u8, buf[0..n], "Hello, world."));
    // もしもわからない場合でも長さを数えてスライスを作れる
    const s = std.mem.sliceTo(&buf, 0);
    debug.print("s.len={d} s=[{s}]\n", .{ s.len, s });
    try expect(eql(u8, s, "Hello, world."));
}

テストの実行結果

$ zig test -lc string_test.zig 
Test [1/1] test.string conversion... 
n=13 output=[Hello, world.]
s.len=13 s=[Hello, world.]
All 1 tests passed.

Discussion