動くコード。
https://github.com/ousttrue/zig-opengl-wasm
- zig で OpenGL する(Windows11 + GLFW
@cImport
の使い方, c のライブラリとのリンクの仕方) - engine を dll に分離する(zig で dll を作る方法。
export
,extern
) - engine を wasm 化する(
target = wasm32-freestanding
)
という記事です。
- engine 部分を単一のコードで desktop と wasm 共用にできる!
更新
- 202304
zig-0.11.0-dev.2336
zig 0.10.0-dev.3952+9e070b653
実行環境
- zig-0.11.0-dev.2336 に更新
- glfw を cmake で dynamic library 化するのをやめて、zig のプロジェクトに直接組み込むように構成を変更
- [desktop] Ubuntu22.04 動いた
- [desktop] Windows11 動いた
- [desktop] wsl 微妙に動かない。OpenGL が真っ黒
- [wasm]ubuntu 22.04 + firefox 動いた
- [wasm]Windows11 + chrome 動いた
その1: GLFW window を glClear
https://www.glfw.org/documentation.html
を移植する。
zig のプロジェクト作成
$ mkdir zig-opengl-wasm # project-root
$ cd zig-opengl-wasm
zig-opengl-wasm$ mkdir desktop
zig-opengl-wasm$ cd desktop
zig-opengl-wasm/desktop $ zig init-exe
GLFW のソースを build.zig
に取り込む
zig-opengl-wasm/desktop$ git submodule add https://github.com/glfw/glfw.git
// exe.install の前に追加
// glfw
exe.linkLibC(); // System ライブラリを有効にする。WindowsKits などへの Include も有効になる
exe.addIncludePath("glfw/include"); // @cInclude がヘッダーを見つけられるように
exe.addCSourceFiles // glfw のソースとコンパイルフラグ
exe.linkSystemLibrary("OpenGL32"); // 必要なものを適当に
exe.linkSystemLibrary("Gdi32");
``
https://github.com/ousttrue/zig-opengl-wasm/blob/801422e310f1fd68b260ddb4ca3506f7ec00fb10/desktop/build.zig#L25-L50
## 関数を `@cImport` 経由で呼び出し
- exe.linkLibC()
- exe.addIncludePath("glfw/include");
でコンパイルできる。
- exe.linkLibC()
- exe.linkSystemLibrary("Gdi32");
でリンクできる。
```zig:main.zig
const std = @import("std");
const c = @cImport({
@cInclude("GLFW/glfw3.h");
});
pub fn main() !void {
std.debug.assert(c.glfwInit() != 0);
defer c.glfwTerminate();
}
vscode のデバッグ設定例
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "desktop",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}/zig-out/bin/desktop",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
{
"name": "PATH",
"value": "${workspaceFolder}\\build\\src\\Debug;${env:PATH}" // dll にパスを通す
}
],
"console": "integratedTerminal"
}
]
}
実行して 終了コードが 0
になれば成功。
dll に見つからなかったりすると 0
以外が帰って異常終了になる。
glClear まで
https://www.glfw.org/documentation.html
を移植。
const std = @import("std");
const c = @cImport({
@cInclude("GLFW/glfw3.h");
});
pub fn main() !void {
// Initialize the library
std.debug.assert(c.glfwInit() == 1);
defer c.glfwTerminate();
// Create a windowed mode window and its OpenGL context
const window = c.glfwCreateWindow(640, 480, "Hello World", null, null);
defer c.glfwDestroyWindow(window);
// Make the window's context current
c.glfwMakeContextCurrent(window);
// Loop until the user closes the window
while (c.glfwWindowShouldClose(window) == 0) {
// Render here
c.glClear(c.GL_COLOR_BUFFER_BIT);
// Swap front and back buffers
c.glfwSwapBuffers(window);
// Poll for and process events
c.glfwPollEvents();
}
}
実行。黒い glfw Window が出れば成功。
その2: glsl で triangle
https://www.glfw.org/docs/latest/quick_guide.html#quick_example
を移植する。
OpenGL4 の glsl を使うので、glad で関数をロードする必要がある。
const std = @import("std");
const c = @cImport({
@cInclude("glad/gl.h");
@cDefine("GLFW_INCLUDE_NONE", &.{});
@cInclude("GLFW/glfw3.h");
});
const Vertex = std.meta.Tuple(&.{ f32, f32, f32, f32, f32 });
const vertices = [3]Vertex{
.{ -0.6, -0.4, 1.0, 0.0, 0.0 },
.{ 0.6, -0.4, 0.0, 1.0, 0.0 },
.{ 0.0, 0.6, 0.0, 0.0, 1.0 },
};
// 外部ファイルを読み込んで `[]const u8` 化する。テキストやバイナリなんでもOK
const vertex_shader_text: [*:0]const u8 = @embedFile("./shader.vs");
const fragment_shader_text: [*:0]const u8 = @embedFile("./shader.fs");
pub fn main() anyerror!void {
// Initialize the library
std.debug.assert(c.glfwInit() == 1);
defer c.glfwTerminate();
// Create a windowed mode window and its OpenGL context
const window = c.glfwCreateWindow(640, 480, "Hello World", null, null);
std.debug.assert(window != null);
defer c.glfwDestroyWindow(window);
// Make the window's context current
c.glfwMakeContextCurrent(window);
_ = c.gladLoadGL(c.glfwGetProcAddress); // glad による OpenGL 初期化
c.glfwSwapInterval(1);
var vertex_buffer: c.GLuint = undefined;
c.glGenBuffers(1, &vertex_buffer);
c.glBindBuffer(c.GL_ARRAY_BUFFER, vertex_buffer);
c.glBufferData(c.GL_ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, c.GL_STATIC_DRAW);
const vertex_shader = c.glCreateShader(c.GL_VERTEX_SHADER);
c.glShaderSource(vertex_shader, 1, &vertex_shader_text, null);
c.glCompileShader(vertex_shader);
const fragment_shader = c.glCreateShader(c.GL_FRAGMENT_SHADER);
c.glShaderSource(fragment_shader, 1, &fragment_shader_text, null);
c.glCompileShader(fragment_shader);
const program = c.glCreateProgram();
c.glAttachShader(program, vertex_shader);
c.glAttachShader(program, fragment_shader);
c.glLinkProgram(program);
const mvp_location = c.glGetUniformLocation(program, "MVP");
const vpos_location = c.glGetAttribLocation(program, "vPos");
const vcol_location = c.glGetAttribLocation(program, "vCol");
c.glEnableVertexAttribArray(@intCast(c_uint, vpos_location));
c.glVertexAttribPointer(@intCast(c_uint, vpos_location), 2, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), null);
c.glEnableVertexAttribArray(@intCast(c_uint, vcol_location));
c.glVertexAttribPointer(@intCast(c_uint, vcol_location), 3, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), @intToPtr(*anyopaque, @sizeOf(f32) * 2));
// Loop until the user closes the window
while (c.glfwWindowShouldClose(window) == 0) {
var width: c_int = undefined;
var height: c_int = undefined;
c.glfwGetFramebufferSize(window, &width, &height);
// ratio = width / (float) height;
// Render here
c.glViewport(0, 0, width, height);
c.glClear(c.GL_COLOR_BUFFER_BIT);
var mvp = [_]f32{
1, 0, 0, 0, //
0, 1, 0, 0, //
0, 0, 1, 0, //
0, 0, 0, 1, //
};
c.glUseProgram(program);
c.glUniformMatrix4fv(mvp_location, 1, c.GL_FALSE, &mvp);
c.glDrawArrays(c.GL_TRIANGLES, 0, 3);
// Swap front and back buffers
c.glfwSwapBuffers(window);
// Poll for and process events
c.glfwPollEvents();
}
}
// exe.linkSystemLibrary("OpenGL32"); // glad 使うので不要
// glad
exe.addIncludePath("glfw/deps");
exe.addCSourceFile("glfw/deps/glad_gl.c", &.{});
実行。三角形が出れば成功。
その3: wasm 化準備
以下のように Engine(OpenGL描画) 部分を dll / wasm
なライブラリとする。
+---------+
|Engine |同じ zig ソースから dll と wasm にビルドする
+---------+
^ ^
|dll |wasm
+-------+ +-------+
|Desktop| |Browser|
|GLFW | |WebGL |
+-------+ +-------+
zig で lib プロジェクトを作る
zig-opengl-wasm$ mkdir engin
zig-opengl-wasm$ cd engin
zig-opengl-wasm/desktop $ zig init-lib
dll には下記の2つの関数を実装する。
extern fn ENGINE_init(p: *const anyopaque) callconv(.C) void;
extern fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void;
GLFW側
OpenGL しない。Window 管理のみ。
const std = @import("std");
const c = @cImport({
@cDefine("GLFW_INCLUDE_NONE", &.{});
@cInclude("GLFW/glfw3.h");
});
// engine の dll の関数宣言。body 無し
extern fn ENGINE_init(p: *const anyopaque) callconv(.C) void;
extern fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void;
pub fn main() anyerror!void {
// Initialize the library
std.debug.assert(c.glfwInit() == 1);
defer c.glfwTerminate();
// Create a windowed mode window and its OpenGL context
const window = c.glfwCreateWindow(640, 480, "Hello World", null, null);
std.debug.assert(window != null);
defer c.glfwDestroyWindow(window);
// Make the window's context current
c.glfwMakeContextCurrent(window);
ENGINE_init(c.glfwGetProcAddress);
c.glfwSwapInterval(1);
// Loop until the user closes the window
while (c.glfwWindowShouldClose(window) == 0) {
// Poll for and process events
c.glfwPollEvents();
var width: c_int = undefined;
var height: c_int = undefined;
c.glfwGetFramebufferSize(window, &width, &height);
// ratio = width / (float) height;
ENGINE_render(width, height);
// Swap front and back buffers
c.glfwSwapBuffers(window);
}
}
dll の実装
OpenGL だけやる。GLFW は見えない。
const std = @import("std");
const c = @cImport({
@cInclude("glad/gl.h");
});
const Vertex = std.meta.Tuple(&.{ f32, f32, f32, f32, f32 });
const vertices = [3]Vertex{
.{ -0.6, -0.4, 1.0, 0.0, 0.0 },
.{ 0.6, -0.4, 0.0, 1.0, 0.0 },
.{ 0.0, 0.6, 0.0, 0.0, 1.0 },
};
const vertex_shader_text: [*:0]const u8 = @embedFile("./shader.vs");
const fragment_shader_text: [*:0]const u8 = @embedFile("./shader.fs");
var program: u32 = undefined;
var mvp_location: c_int = undefined;
export fn ENGINE_init(p: *anyopaque) callconv(.C) void {
_ = c.gladLoadGL(@ptrCast(?*const fn([*c]const u8) callconv(.C) ?*const fn() callconv(.C) void, p));
var vertex_buffer: c.GLuint = undefined;
c.glGenBuffers(1, &vertex_buffer);
c.glBindBuffer(c.GL_ARRAY_BUFFER, vertex_buffer);
c.glBufferData(c.GL_ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, c.GL_STATIC_DRAW);
const vertex_shader = c.glCreateShader(c.GL_VERTEX_SHADER);
c.glShaderSource(vertex_shader, 1, &vertex_shader_text, null);
c.glCompileShader(vertex_shader);
const fragment_shader = c.glCreateShader(c.GL_FRAGMENT_SHADER);
c.glShaderSource(fragment_shader, 1, &fragment_shader_text, null);
c.glCompileShader(fragment_shader);
program = c.glCreateProgram();
c.glAttachShader(program, vertex_shader);
c.glAttachShader(program, fragment_shader);
c.glLinkProgram(program);
mvp_location = c.glGetUniformLocation(program, "MVP");
const vpos_location = c.glGetAttribLocation(program, "vPos");
const vcol_location = c.glGetAttribLocation(program, "vCol");
c.glEnableVertexAttribArray(@intCast(c_uint, vpos_location));
c.glVertexAttribPointer(@intCast(c_uint, vpos_location), 2, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), null);
c.glEnableVertexAttribArray(@intCast(c_uint, vcol_location));
c.glVertexAttribPointer(@intCast(c_uint, vcol_location), 3, c.GL_FLOAT, c.GL_FALSE, @sizeOf(Vertex), @intToPtr(*anyopaque, @sizeOf(f32) * 2));
}
export fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void {
// Render here
c.glViewport(0, 0, width, height);
c.glClear(c.GL_COLOR_BUFFER_BIT);
var mvp = [_]f32{
1, 0, 0, 0, //
0, 1, 0, 0, //
0, 0, 1, 0, //
0, 0, 0, 1, //
};
c.glUseProgram(program);
c.glUniformMatrix4fv(mvp_location, 1, c.GL_FALSE, &mvp);
c.glDrawArrays(c.GL_TRIANGLES, 0, 3);
}
openg gl 関数を extern 関数化
wasm 化したときの WebGL 呼び出しと、native glfw の OpenGL4(glad) 呼び出し共通化するために、
OpenGL のラッパーを作る。
ラッパーはただの extern 関数
で、wasm のときは javascript の関数をリンクし、native ビルドのときは c の関数をリンクするようにできる。
extern fn some(int a) callconv(.C) int;
https://github.com/MasterQ32/zig-opengl
こういう感じで OpenGL-Registry からコード生成して glad も自前にするとよいかも
js のリンク
fucntion some_js(a){ return a + 1; }
// 初期化の引数
const importObject = {
env {
some: some_js,
}
};
// wasm の初期化時に extern 関数をリンクする
const instance = await WebAssembly.instantiate(compiled, importObject);
c のリンク
int some(int a){ return a+1; }
if (target.cpu_arch != std.Target.Cpu.Arch.wasm32) { // <- デスクトップのときだけ glad から供給する
// glad
lib.linkLibC();
lib.addCSourceFile("src/glad_placeholders.c", &.{}); // OpenGL ラッパー関数の本体を記述。glad の関数に呼び替える
}
gl のラッパーを作成
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const lib = b.addSharedLibrary("engine", "src/main.zig", .unversioned);
lib.setTarget(target);
lib.setBuildMode(mode);
if (target.cpu_arch != std.Target.Cpu.Arch.wasm32) { // <- デスクトップのときだけ glad から供給する
// glad
lib.linkLibC();
lib.addIncludePath("../desktop/glfw/deps");
lib.addCSourceFile("../desktop/glfw/deps/glad_gl.c", &.{});
lib.addCSourceFile("src/glad_placeholders.c", &.{}); // <- これ
}
lib.install();
const main_tests = b.addTest("src/main.zig");
main_tests.setBuildMode(mode);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&main_tests.step);
}
ビルドして dll 版が動くのを確認する。
その4 zig の wasm build
build.zig を使う方法
よく紹介されている方法で以下のようなコマンドラインを使う方法がある。
$ zig build-lib -target wasm32-wasi src/main.zig
zig build
じゃなくて zig build-lib
(zig build-exe
) を使っている。
これは zig ファイルを一個ずつコンパイルする方法で、
build.zig
が使われません。
@cImport
や自前定義の Pkg
が絡むと長いコマンドラインが必要になります。
build.zig
でターゲットを指定する方法を説明する。
const target = b.standardTargetOptions(.{});
lib.setTarget(target);
により -Dtarget=
が有効になります。
ターゲットには wasm32-freestanding
を選択する。
wasm32-wasi
などもできるが、今回は Import 関数を少なくしたいので wasm32-freestanding
を採用します。
engine$ zig build -Dtarget=wasm32-freestanding
ビルド => zig-out/lib/engine.wasm
ビルド自体はさくっとできる。
export シンボルの明示が必要
breaking change!
https://github.com/ziglang/zig/issues/14139
wasm ビルドに以下の対応が必要
lib.rdynamic = true;
lib.export_symbol_names = &[_][]const u8{
"ENGINE_init",
"ENGINE_render",
"ENGINE_getGlobalInput",
};
その5 chrome で wasm を実行
参考
https://github.com/fabioarnold/hello-webgl
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="index.css">
<script type="module" src="index.js"></script>
</head>
<body>
<canvas id="gl"></canvas>
</body>
</html>
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
canvas#gl {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
index.js
zig のとこよりこっちのほうが量が多いかも。
とりあえず wasm 動かしてみる
const importObject = {
env: {
},
};
// get
const response = await fetch('zig-out/lib/engine.wasm') // index.html からの相対パスで
// byte array
const buffer = await response.arrayBuffer();
// compile
const compiled = await WebAssembly.compile(buffer);
// compile の次で debugger で止める
compiled.Exports
4つあります。
zig で export した公開関数が 3つ と memory です。
export fn ENGINE_init(p: *anyopaque) callconv(.C) void;
export fn ENGINE_render(width: c_int, height: c_int) callconv(.C) void;
export fn ENGINE_getGlobalInput() callconv(.C) *u8; // 追加で作った固定バッファ。jsからwasmに文字列をコピーする
callconv(.C)
はwasm
には無関係。あってもなくても動きます。desktop の dll 向けでextern "C"
に相当。
compiled.Imports
18あります。
zig で extern 宣言した OpenGL 関数郡です。
https://github.com/ousttrue/zig-opengl-wasm/blob/master/engine/src/gl.zig#L78-L123
これらの関数は, js の instanciate の 引数として wasm に渡してやります。
// instanciate env に webgl などを埋め込む
const instance = await WebAssembly.instantiate(compiled, importObject);
まだ、作っていないので以下のエラーが出ます。
Uncaught LinkError: WebAssembly.instantiate(): Import #0 module="env" function="genBuffers" error: function import requires a callable
その6 importObject.env に webgl 関数を供給する
https://github.com/ousttrue/zig-opengl-wasm/blob/master/engine/index.js#L115
細かいところはソースを見てもらうとして、 WebGL と OpenGL4 のギャップを埋める方法について
canvas から gl context を作って、それの関数を呼び出すクロージャー
愚直に OpenGL4 仕様で実装する。
- 文字列の0terminate依存
- int, uint
- pointer返し、値返し
- (void*)型のoffset
あたりは変えたほうが、zig での扱いが楽かもしれぬ。
object と id の変換
js側で array に蓄えて、index を id に変換する。
0 が無効値を表すものは、1 origin になるように id=index+1
とするとよい。
wasm のメモリ空間
wasm のメモリ空間は WebAssembly.instantiate 後に確定して instance.exports.memory.buffer
になる。
この中に、wasm の stack や heap, グローバル変数が全部入っている。
build.zig でスタックサイズを指定できる。
const lib = b.addSharedLibrary("zig_renderer", "src/main.zig", .unversioned);
if (target.cpu_arch == std.Target.Cpu.Arch.wasm32) {
lib.stack_size = 6 * 1024 * 1024;
}
wasm のメモリ空間は Uint8Array
で、
ポインタはそれに対する index である。
js から メモリを書き換える
wasm の外からは instance.exports.memory.buffer
のどこに何があるか分からないので、
書き込み先のポインターを wasm の export 関数からもらう。
// グローバル変数に固定長のバッファを確保する手抜き例
// malloc のような関数を公開する手もある
var buffer: [1024]u8 = undefined;
export fn ENGINE_getGlobalInput() callconv(.C) *u8 {
return &buffer[0];
}
const dstPtr = instance.exports.getGlobalAddress();
const memory = instance.exports.memory.buffer;
// memory[dstPtr] が zig の buffer[0] に相当する。あとは適当に内容を更新する
その他
zig の logger 出力を browser の console に接続する
早めにやっておくと print debug が捗る。
pub extern fn console_logger(level: c_int, ptr: *const u8, size: c_int) void;
fn extern_write(level: c_int, m: []const u8) error{}!usize {
if (m.len > 0) {
console_logger(level, &m[0], @intCast(c_int, m.len));
}
return m.len;
}
pub fn log(
comptime message_level: std.log.Level,
comptime scope: @Type(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
if (builtin.target.cpu.arch == .wasm32) { // <- wasm のときだけ extern 関数にリダイレクトする
const level = switch (message_level) {
.err => 0,
.warn => 1,
.info => 2,
.debug => 3,
};
const w = std.io.Writer(c_int, error{}, extern_write){
.context = level,
};
w.print(format, args) catch |err| {
const err_name = @errorName(err);
extern_write(0, err_name) catch unreachable;
};
_ = extern_write(level, "\n") catch unreachable;
} else {
std.log.defaultLog(message_level, scope, format, args);
}
}
chrome wasm デバッガー
zig のソースにステップインしてステップ実行できた。
- https://chrome.google.com/webstore/detail/cc%20%20-devtools-support-dwa/pdcpmagijalfljmkmjngeonclgbbannb
- https://developer.chrome.com/blog/wasm-debugging-2020/
github action で wasm ビルドして github-pages で動かす
終わり
Three-js とか Babylon.js のようなものを zig で作れるような気がする。