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で文字列をやり取りする際は生のポインタを渡す必要があるというところ
理由は
- Cの文字列はNULL(sentinel)で終端されたただのバイト列(の先頭へのポインタ)である
- Zigの文字列は文字列を表すバイト列の先頭へのポインタと文字列サイズを持つ
つまり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アプリ作ったりするのもいいけど、どんどん低レイヤーに走っていくのも楽しいですね