Skip to content
Go back

libpqをZigから使う

Edit page

ZigからPostgreSQLを使いたかったけど、zig nativeなライブラリで良さげなのが見つからなかったのでとりあえず枯れているlibpqを使って見ることにした

Cのライブラリを簡単に使えるのがZigのいいところ

ヘッダーファイルのインポート

Zigでは@cImportを使ってヘッダーファイルを参照することができます

const c = @cImport({
    @cInclude("/opt/homebrew/opt/libpq/include/libpq-fe.h");
});

パスの指定はなんかもっとうまい具合にできそうな気がする

libpqをリンクする

上記ができるとlibpqのヘッダファイルが読めるようになるのでZigから使えるようになります

LSPでも補完がでるようになる

ただこれではライブラリの実体はリンクできてないので、別途 build.zig で設定をする必要があります

以下の行を追加します

exe は b.addExecutable で帰ってくるstd.Build.Step.Compileへのポインタです

    exe.root_module.linkSystemLibrary("pq", .{});

これでビルドできるようになりました

PostgreSQLに接続する

libpqの使い方は公式ドキュメント参照

公式ドキュメントをみてそれっぽい関数を探す

接続はこんな感じでやってみた

ConnはZig側で定義したCのコネクションを表す構造体のラッパーです

// connStringは実際には[]u8にして動的な文字列にすることが多いはず
// c.PQfinishは別途する必要がある
fn connect(connString: []const u8) !Conn {
    // PGConnectdbのZigでの型定義
    // [*c]const u8 はC文字列を表します
    // pub extern fn PQconnectdb(conninfo: [*c]const u8) ?*PGconn;
    const conn = c.PQconnectdb(connString.ptr);
    if (conn) |pgConn| {
        const status = c.PQstatus(pgConn);
        switch (status) {
            c.CONNECTION_OK => std.debug.print("Connection successful!\n", .{}),
            else => {
                const errMsg = c.PQerrorMessage(pgConn);
                std.debug.print("Connection error: {s}\n", .{errMsg});
                return error.ConnectionFailed;
            },
        }
        return Conn{ .pg_conn = pgConn };
    } else {
        return error.ConnectionFailed;
    }
}

あまりCに馴染みがないので最初はパッとわからなかったところは、Zig ↔︎ Cで文字列をやり取りする際は生のポインタを渡す必要があるというところ

理由は

つまりZigからCに文字列を渡したい時は、ポインタだけを渡す必要があるし、逆にCからZigに文字列を渡す場合は、Zig側で受け取った文字列のサイズを測ってZigの文字列に変換する必要がある

C → Zig

    // Cから文字列を受け取る
    const cstr: [*c]u8 = someCFn();
    // std.mem.spanはNULLまでメモリを読んでくれる
    const zigStr: []u8 = std.mem.span(cstr);

Zig → C

    const zigStr: []const u8 = "hello world!";
    // .ptrは生の文字列へのポインタ
    const cStr = zigStr.ptr;
    // CのAPIにわたす
    someCFn(cStr);

クエリをする

// 今回は一番シンプルにPQexecを使用
// PQexecParams とか PQprepare -> PQexecPrepared とかも使えるはず
pub fn exec(conn: Conn, sql: []u8) !void {
    std.debug.print("Running query...\n", .{});
    var cmd_buffer: [256]u8 = undefined;
    const res = c.PQexec(conn.pg_conn, sql);
    dumpResultInfo(meta, res);
    std.debug.print("Query result: {?}\n", .{res});
}

結果を表示する

これが結構めんどくさい

とりあえずただ結果をみてみたかったのでMarkdownで出力してみた

pub fn dumpResultInfo(res: ?*c.struct_pg_result) void {
    if (res == null) {
        std.debug.print("No result returned from query.\n", .{});
        return;
    }
    const nFields: usize = @intCast(c.PQnfields(res));
    const nTuples: usize = @intCast(c.PQntuples(res));
    for (0..nFields) |i| {
        const ci: c_int = @intCast(i);
        const fieldName = stringFromCPtr(c.PQfname(res, ci));
        std.debug.print(" | {s}", .{ fieldName });
    }
    std.debug.print(" |\n", .{});
    for (0..nFields) |_| {
        std.debug.print(" | --- ", .{});
    }
    std.debug.print(" |\n", .{});

    for (0..nTuples) |i| {
        const ci: c_int = @intCast(i);
        for (0..nFields) |j| {
            const cj: c_int = @intCast(j);
            const value = stringFromCPtr(c.PQgetvalue(res, ci, cj));
            std.debug.print(" | {s}", .{value});
        }
        std.debug.print(" |\n", .{});
    }
}

基本的にはここらへんに書いてある関数を使って結果取得をするらしい

PQgetvalueに何番目の行の何番目の列のデータを取得するかを指定すると、文字列で結果が帰ってくる

これを元に文字列以外の型にする場合は別途アプリ側で変換する必要があるみたい

普段はやっぱりこういうのを全部やってくれるドライバを使うことしかないのでそういうソフトウェアのありがたさを感じますね

まとめ

Zigからlibpqを使ってみました

Zigみたいな新しめの言語だとエコシステムが充実していない部分も多々あるので既存の資産を簡単に使えるというのはありがたいです

まだまだメモリのレイアウトを意識したり、管理を自分でやる必要のあるlow levelプログラミングに慣れていないので精進したいところ

(今回みたいなのをlow levelと言っていいかはさておき)

Webアプリ作ったりするのもいいけど、どんどん低レイヤーに走っていくのも楽しいですね


Edit page
Share this post on:

Previous Post
SwiftからZigのライブラリを使う
Next Post
Supabaseを使う場合はRLSは必須だよ、という話