はじめに
ZigでWin32APIを利用してウィンドウを表示します。
以下のような表示を目指します。
開発環境
zig version
0.13.0
zigwin32を使う場合
win32metadata から自動生成されたバインディングである zigwin32 を使う場合です。
まずはプロジェクトを初期化します。
mkdir zig-window
cd zig-window
zig init
echo .zig-cache > .gitignore
echo zig-out >> .gitignore
git init
git add .
git commit -m "Add: 最初のコミット"
次に、zigwin32
を追加します。
リポジトリの下にmodsフォルダを作って、そこにサブモジュールを置きます。
mkdir mods
cd mods
git submodule add https://github.com/marlersoft/zigwin32
git commit -m "Add: モジュールを追加"
依存関係をファイルに記述します。
.{
// 中略...
.dependencies = .{
.zigwin32 = .{ .path = "mods/zigwin32" },
},
}
ビルドスクリプトにも記述します。
pub fn build(b: *std.Build) void {
// 中略...
const exe = b.addExecutable(.{ ... });
const zigwin32 = b.dependency("zigwin32", .{});
exe.root_module.addImport("zigwin32", zigwin32.module("zigwin32"));
}
これでWin32APIを利用する準備が整いました。
冒頭のようにただウィンドウを表示するだけならこれでOKです。
const std = @import("std");
pub const UNICODE: bool = true;
const zigwin32 = @import("zigwin32");
const win_messaging = zigwin32.ui.windows_and_messaging;
const win_zig = zigwin32.zig;
const win_foundation = zigwin32.foundation;
const win_libloader = zigwin32.system.library_loader;
const WINAPI = @import("std").os.windows.WINAPI;
fn WindowProcedure(hwnd: win_foundation.HWND, msg: u32, wparam: win_foundation.WPARAM, lparam: win_foundation.LPARAM) callconv(WINAPI) win_foundation.LRESULT {
if (msg == win_messaging.WM_DESTROY) {
win_messaging.PostQuitMessage(0);
return 0;
}
return win_messaging.DefWindowProc(hwnd, msg, wparam, lparam);
}
pub fn main() !void {
var w = std.mem.zeroes(win_messaging.WNDCLASSEX);
w.cbSize = @sizeOf(win_messaging.WNDCLASSEX);
w.lpfnWndProc = WindowProcedure;
w.lpszClassName = win_zig._T("HelloWorld");
w.hInstance = win_libloader.GetModuleHandle(null);
_ = win_messaging.RegisterClassEx(&w);
var wrc: win_foundation.RECT = .{ .left = 0, .top = 0, .right = 300, .bottom = 200 };
_ = win_messaging.AdjustWindowRect(&wrc, win_messaging.WS_OVERLAPPEDWINDOW, 0);
const hwnd: ?win_foundation.HWND = win_messaging.CreateWindowEx(win_messaging.WS_EX_LEFT, w.lpszClassName, win_zig._T("HelloWorld"), win_messaging.WS_OVERLAPPEDWINDOW, 0, 0, wrc.right - wrc.left, wrc.bottom - wrc.top, null, null, w.hInstance, null);
_ = win_messaging.ShowWindow(hwnd, win_messaging.SW_SHOW);
while (true) {
var msg = std.mem.zeroes(win_messaging.MSG);
if (win_messaging.PeekMessage(&msg, null, 0, 0, win_messaging.PM_REMOVE) != 0) {
_ = win_messaging.TranslateMessage(&msg);
_ = win_messaging.DispatchMessage(&msg);
}
if (msg.message == win_messaging.WM_QUIT) {
break;
}
}
_ = win_messaging.UnregisterClass(w.lpszClassName, w.hInstance);
_ = win_messaging.DestroyWindow(hwnd);
}
test "simple test" {
var list = std.ArrayList(i32).init(std.testing.allocator);
defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
try list.append(42);
try std.testing.expectEqual(@as(i32, 42), list.pop());
}
下記コマンドで動作を確認できます。
zig build run
小ネタ
コード中では特に使っていないように見える UNICODE
という変数ですが、実はこれがないとビルドできません。
zigwin32/win32/zig.zig
を参照すると分かるのですが、この変数がないと内部的に未指定モードになり、@compileErrorによってビルドに失敗する仕組みになっています。
const root = @import("root");
pub const UnicodeMode = enum { ansi, wide, unspecified };
pub const unicode_mode: UnicodeMode = if (@hasDecl(root, "UNICODE")) (if (root.UNICODE) .wide else .ansi) else .unspecified;
@import() で得た type オブジェクトを @hasDecl に渡すと対象に任意の名前の宣言が存在するかチェックできるようで、面白い仕組みです。
そして@import("root") のときは恐らくエントリポイントのファイルが指定される?っぽいです。
追記:
やはり"root"のときだけ特別扱いされているようです。
https://ziglang.org/documentation/master/#import
zwin32を使う場合
ゲーム開発向けに作られたバインディングがあるようで、こちらも試してみました。
zwin32 というものです。
こちらもまずはプロジェクトを初期化します。
mkdir zig-game
cd zig-game
zig init
echo .zig-cache > .gitignore
echo zig-out >> .gitignore
git init
git add .
git commit -m "Add: 最初のコミット"
次に、zwin32
を追加します。
リポジトリの下にmodsフォルダを作って、そこにサブモジュールを置きます。
mkdir mods
cd mods
git submodule add https://github.com/zig-gamedev/zwin32
git commit -m "Add: モジュールを追加"
同様に依存関係をファイルに記述します。
.{
// 中略...
.dependencies = .{
.zwin32 = .{ .path = "mods/zwin32" },
},
}
ビルドスクリプトにも記述します。
pub fn build(b: *std.Build) void {
// 中略...
const exe = b.addExecutable(.{ ... });
const zwin32 = b.dependency("zwin32", .{});
exe.root_module.addImport("zwin32", zwin32.module("root"));
@import("zwin32").install_xaudio2(&exe.step, .bin);
@import("zwin32").install_d3d12(&exe.step, .bin);
@import("zwin32").install_directml(&exe.step, .bin);
}
動作結果は同じですが、APIが微妙に異なるのでこちらもコードを下記に載せます。
const std = @import("std");
const zwin32 = @import("zwin32");
const w32 = zwin32.w32;
const dwrite = zwin32.dwrite;
const dxgi = zwin32.dxgi;
const d3d12 = zwin32.d3d12;
const d3d12d = zwin32.d3d12d;
const dml = zwin32.directml;
fn WindowProcedure(hwnd: w32.HWND, msg: w32.UINT, wparam: w32.WPARAM, lparam: w32.LPARAM) callconv(w32.WINAPI) w32.LRESULT {
if (msg == w32.WM_DESTROY) {
w32.PostQuitMessage(0);
return 0;
}
return w32.DefWindowProcA(hwnd, msg, wparam, lparam);
}
pub fn main() !void {
var w = std.mem.zeroes(w32.WNDCLASSEXA);
w.cbSize = @sizeOf(w32.WNDCLASSEXA);
w.lpfnWndProc = WindowProcedure;
w.lpszClassName = "HelloWorld";
w.hInstance = @ptrCast(w32.GetModuleHandleA(null));
_ = w32.RegisterClassExA(&w);
var wrc: w32.RECT = .{ .left = 0, .top = 0, .right = 300, .bottom = 200 };
_ = w32.AdjustWindowRectEx(&wrc, w32.WS_OVERLAPPEDWINDOW, 0, 0x00000000);
const hwnd: ?w32.HWND = w32.CreateWindowExA(0x00000000, w.lpszClassName, "HelloWorld", w32.WS_OVERLAPPEDWINDOW | w32.WS_VISIBLE, 0, 0, wrc.right - wrc.left, wrc.bottom - wrc.top, null, null, w.hInstance, null);
if (hwnd) |hwndInst| {
while (true) {
var msg: w32.MSG = std.mem.zeroes(w32.MSG);
if (w32.PeekMessageA(&msg, null, 0, 0, w32.PM_REMOVE) != 0) {
_ = w32.TranslateMessage(&msg);
_ = w32.DispatchMessageA(&msg);
}
if (msg.message == w32.WM_QUIT) {
break;
}
}
_ = w32.DestroyWindow(hwndInst);
}
}
test "simple test" {
var list = std.ArrayList(i32).init(std.testing.allocator);
defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
try list.append(42);
try std.testing.expectEqual(@as(i32, 42), list.pop());
}
cInclude/cImportを使う場合
せっかくZigにはC言語との連携機能があるのですから、それを使えばいいように思います。
const windows = @cImport(@cInclude("Windows.h"));
これだけではビルドに失敗します。
src\main.zig:2:17: error: C import failed
const windows = @cImport(@cInclude("Windows.h"));
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src\main.zig:2:17: note: libc headers not available; compilation does not link against libc
なので、メッセージに従いlibcをリンクさせます。
exe.linkLibC();
すると今度は別のメッセージが表示され、失敗します。
error: unable to translate macro: undefined identifier
A
pub const __MINGW_NAME_AW = @compileError("unable to translate macro: undefined identifierA
");
これを解決する方法が分からなかったので、この方法は諦めました。