Open7

zigではまったもの

Mitsuhiro KogaMitsuhiro Koga

開発環境構築

2024/04/17時点で0.12系を使うための手順

バージョン管理ツールのzigupをインストールする
Windows + scoopの場合

  1. scoopでzigupをインストールする
    scoop install zigup
    
  2. 環境変数PATHへ %HOMEPATH%\.local\bin を追加する

macOSの場合

  1. https://github.com/marler8997/zigup/releases/ から最新版をダウンロード、展開して zigup~/.local/bin へコピー、chmod +x ~/.local/bin/zigup する
  2. ~/.zprofile などのシェルの設定ファイルに以下を追記する
    export PATH=$HOME/.local/bin:$PATH
    
  3. zigup を実行するとブロックされるのでシステム設定 > プライバシーとセキュリティからzigupを許可する
  4. 再度 zigup を実行すると「“zigup”が悪質なソフトウェアかどうかをAppleでは確認できないため、このソフトウェアは開けません。」のダイアログが出るので「開く」を選択する

zigup master で0.13系zigをインストール


zigのバージョンに合わせたzlsをインストールする

Windows + Powershellの場合

git clone https://github.com/zigtools/zls.git
cd zls
zig build -Doptimize=ReleaseSafe
mkdir ~\.local\bin
cp zig-out\bin\zls.exe ~\.local\bin

macOSの場合

git clone https://github.com/zigtools/zls.git
cd zls
zig build -Doptimize=ReleaseSafe
mkdir -p ~/.local/bin
cp zig-out/bin/zls ~/.local/bin
chmod +x ~/.local/bin/zls

zipup masterのバージョン確認

zigup fetch-index で実行時点でインストールできるバージョンがjsonで出力される。
jq と組み合わせて zigup fetch-index | jq '.master.version' で確認できる。

Mitsuhiro KogaMitsuhiro Koga

neovim向けにzlsを使う

Plug 'neovim/nvim-lspconfig'
Plug 'ziglang/zig.vim'

let g:mapleader = ","

:lua << EOF

-- Setup language servers
local lspconfig = require('lspconfig')
lspconfig.zls.setup{}

-- Global mappings.
-- See `:help vim.diagnostic.*` for documentation on any of the below functions
vim.keymap.set('n', '<Leader>e', vim.diagnostic.open_float)
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev)
vim.keymap.set('n', ']d', vim.diagnostic.goto_next)
vim.keymap.set('n', '<Leader>q', vim.diagnostic.setloclist)

-- Use an on_attach function to only map the following keys
-- after the language server attaches to the current buffer
local on_attach = function(client, bufnr)
  -- Mappings.
  -- See `:help vim.lsp.*` for documentation on any of the below functions
  local bufopts = { noremap=true, silent=true, buffer=bufnr }
  vim.keymap.set('n', 'K', vim.lsp.buf.hover, bufopts)
  vim.keymap.set('n', 'gd', vim.lsp.buf.definition, bufopts)
  vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, bufopts)
  vim.keymap.set('n', 'gr', vim.lsp.buf.references, bufopts)
  vim.keymap.set('n', '<Leader>f', vim.lsp.buf.format, bufopts)
  vim.keymap.set('n', '<Leader>D', vim.lsp.buf.type_definition, bufopts)
end

-- Use a loop to conveniently call 'setup' on multiple servers and
-- map buffer local keybindings when the language server attaches
local servers = { 'zls' }
for _, lsp in pairs(servers) do
  require('lspconfig')[lsp].setup {
    on_attach = on_attach,
    flags = {
      -- This will be the default in neovim 0.7+
      debounce_text_changes = 150,
    },
    settings = {
      solargraph = {
        diagnostics = false
      }
    }
  }
end
EOF

参考

Mitsuhiro KogaMitsuhiro Koga

テストケースでコマンドライン引数を与えられない

zigでコマンドライン引数を受け取れる std.process.argsAllocstd.os.argv はネイティブAPIを直接呼び出しているのでテストケースから与える事はできない。
zig test -- a b c のようにテスト実行時なら引数を与える事はできる。
そのためmain関数は引数を取り出す所までにしてそれを受け取る関数を切り出してそれをテストするのがベターである。

https://zig.news/xq/zig-build-explained-part-1-59lf

Mitsuhiro KogaMitsuhiro Koga

godot-zigからprintを呼び出す

以下のコードでGodotのコンソールへメッセージを出力できる

Test.zig
const Godot = @import("godot");
const UtilityFunctions = Godot.UtilityFunctions;
const print = UtilityFunctions.print;

const Self = @This();
pub usingnamespace Godot.Object;

base: Godot.Object,

pub fn init(self: *Self) void {
    const name = @typeName(@TypeOf(self));
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer std.debug.assert(gpa.deinit() == .ok);

    const msg = std.fmt.allocPrint(allocator, "init {s}", .{name}) catch @panic("allocPrint");
    defer allocator.free(msg);
    print(v(msg), .{});
}

fn s(str: []const u8) String {
    return String.initFromUtf8Chars(str);
}

fn v(str: []const u8) Variant {
    return Variant.initFrom(s(str));
}

GDScriptからはfree()でメモリ解放しないと終了時にメモリリークエラーが出るので注意

demo.gd
extends Node

var test: Test

func _ready() -> void:
	print("Hello GodotZig!")
	test = Test.new()

func _exit_tree() -> void:
	test.free()

以下のように usingnamespacebaseRefCounted に変えると RefCounted クラスを継承したクラスとして振る舞うので参照がなくなった時点で自動で free されるのでGDScriptからの扱いが楽になる。

Test.zig
const Godot = @import("godot");
const UtilityFunctions = Godot.UtilityFunctions;
const print = UtilityFunctions.print;

const Self = @This();
pub usingnamespace Godot.RefCounted;

base: Godot.RefCounted,

pub fn init(self: *Self) void {
    const name = @typeName(@TypeOf(self));
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    defer std.debug.assert(gpa.deinit() == .ok);

    const msg = std.fmt.allocPrint(allocator, "init {s}", .{name}) catch @panic("allocPrint");
    defer allocator.free(msg);
    print(v(msg), .{});
}

fn s(str: []const u8) String {
    return String.initFromUtf8Chars(str);
}

fn v(str: []const u8) Variant {
    return Variant.initFrom(s(str));
}
Mitsuhiro KogaMitsuhiro Koga

godot-zigでGodot.Stringを[]u8へ変換する

Godot.stringToAscii でStringに格納されている文字列を n へ取り出せる。

const value = String.initFromUtf8Chars("Hello");
var buf: [256]u8 = undefined;
const n = Godot.stringToAscii(value, &buf);
Mitsuhiro KogaMitsuhiro Koga

godot-zigでカスタムクラスにカスタムコンストラクタを定義する

結論。Godot Engine 4.2.2時点のGDExtensionではGDScriptの_init(arg)のような引数付きコンストラクタを作れない。
https://github.com/godotengine/godot-cpp/issues/953

以下は調査の足跡


以下の行はGodot Engine 4.2.2でコンストラクタの判定式

https://github.com/godotengine/godot/blob/4.2.2-stable/modules/gdscript/gdscript_analyzer.cpp#L3296

コンストラクタの場合の後続処理

https://github.com/godotengine/godot/blob/4.2.2-stable/modules/gdscript/gdscript_analyzer.cpp#L3692-L3694

rustのgdextでは以下のようにinit関数からコンストラクタを生成している

https://github.com/godot-rust/gdext/blob/master/godot-macros/src/class/data_models/interface_trait_impl.rs#L97-L120

godot-goの実装ではcreate_callbackで_init(arg)に相当する関数を登録してるっぽい。
あとはfree_callbackとreference_callbackの用途と渡すべきものを調べる。

https://github.com/godot-go/godot-go/blob/v0.3.18/cmd/generate/gdclassinit/classes.init.go.tmpl

  • create_instance_bind: 調査中
  • free_instance_bind: Objectのデストラクタから呼ばれる関数
  • reference_bind: RefCounted::referenceから呼ばれる
  • unreference_bind: RefCounted::unreferenceから呼ばれる
  • binding callbacks
    • create_callback: C#版で必要となってinterfaceが定義されているがGDScript/GDExtensionからは呼ばれてなさそう
    • free_callback: Objectのデストラクタから呼ばれる関数
    • reference_callback: RefCounted::referenceから引数true(1)で呼ばれて、RefCounted::unreferenceから引数false(0)で呼ばれ、破棄可能ならtrue(1)、破棄不可能ならfalse(0)を返すべき関数

p_userdataはGodot.registerClassが呼ばれた時にクラス名の[]u8をStringNameに変換したポインタが格納されている。

GDScriptでCustomClass.new(arg)を呼んだ時に_init(arg)が呼ばれるまでのフローが分かればどうにかなりそう

Mitsuhiro KogaMitsuhiro Koga

関数で文字列の配列を受け取る

const std = @import("std");

fn test_fn(names: []const []const u8) void {
    for (names) |name| {
        std.log.info("{s}", .{name});
    }
}

pub fn main() void {
    test_fn(&[_][]const u8{"aaa", "b", "cccc"});
}