🤖

【Godot4.3】GDExtensionをバインディングライブラリ無しで使ってみる

2024/12/06に公開

この記事はQiita Advent Calender 2024Godot Engine Advent Calender 2024の6日目の記事です。

直近の記事は4日目の@am1tanakaさんの【Godot4.3】Macで2Dゲームの動きがガタついたときにやったこと、次の日はまだ空いていて、後続の記事は@Rutile3さんの記事「【Godot 4.3】乗ると移動するプロックの作り方!」になる予定です。

また、D言語 Advent Calender 2024の6日目の記事という事にもしてしまいました。

前書き

記事の作者について

Godotはまだ駆け出しのおっさんですが、今年あたりから本格的に触り出していて(簡単な2Dゲームですが……)、来年にはインディーゲームのリリースを目指しています。

それで、訳あってGDExtensionを色々触ることになって、あまり資料が無いなと思ったので、備忘録代わりに書いておこうと思います。

GDExtensionについて

https://docs.godotengine.org/ja/4.x/tutorials/scripting/gdextension/what_is_gdextension.html

おおまかに言うと、GDScriptやC#以外のネイティブ言語(CやC++やRust)で、Godotの中で使えるクラスや関数を作れる機能になります。

GDExtensionを扱う情報を漁ったところ、

  • 各言語向けのバインディングライブラリを使う
  • C++向けのバインディングライブラリcodot-cppでも結構環境構築が大変

といった感じが見受けられました。

D言語くん「D言語使えば簡単なのに……。」

という気分になったのと、D言語はこういうときに読みやすくて他言語ユーザーにも分かりやすいメリットがあるので、本記事ではD言語でGDExtensionをバインディングライブラリ無しで使う方法について解説してみようと思います。

なお、後ほどそんなに簡単でもないことが分かるのでした……。

D言語について

https://dlang.org/

挑戦的な名前(C言語の次なのでD言語)のくせにマイナー言語なので今時の若い人は知らない人もいるかもしれませんが、C/C++を整理してGC等の機能も盛り込んだすごいマルチパラダイムネイティブ言語で文法はC/C++/C#系という感じです。

D言語もアドベントカレンダーをやっているので是非見て行ってください。日付の空きもたくさんあります!

https://qiita.com/advent-calendar/2024/dlang

なぜバインディングライブラリ無しでGDExtensionを?

私も当初はgodot-dlangといったバインディングライブラリを使ってみていましたが、

  • バインディングライブラリ自体がGodotのバージョンアップに追随できていない場合がある。マイナー言語で顕著……。
    • GDExtensionがそもそも結構変わる。Godotの4.1と4.3でも大きく違う。
  • GDExtensionの詳細が分かっていないとトラブルがあったときに対応が難しい。
  • そもそも必要な機能はGDExtensionとして提供される内容(基本的にGodotの全クラス)のごく一部だったりする。

といった問題もあると思って、場合によってはバインディングライブラリ無し、または必要なユーティリティを自作しておく選択肢もあるかと愚考しました。

ちなみに、バインディングライブラリgodot-dlangを使った場合の雰囲気はこちらの記事とか参考になるようですね。

https://zenn.dev/outlandkarasu/articles/7fa1a373536d69

今回の前提環境

今回はWindows上でのDLLの開発を前提に解説します。
ただ、D言語であればLinux等であっても大きく手順は変わらないかと思います。

IDEはVSCodeを想定します。とはいえD言語は別にVimとか使っても問題ありません。
Godotのバージョンは4.3-stableとします。

D言語の準備

それでは、なるべくシンプルにD言語でGDExtensionモジュールを作ってみましょう。そのためには、まずD言語でDLLを作れる環境が必要です。

D言語のインストール

たぶんD言語なんて使っていないという人が多いと思うので、まずはインストール方法です。

基本的にこちらの人が書いている記事に従ってWindows版をインストールすればOKです。

https://zenn.dev/outlandkarasu/articles/341f4d3fc9add9

D言語インストールが人生最良の行動とかやばいですね。

D言語Hello,World

D言語のビルドツールdubで簡単にD言語プロジェクトが作れます。

$ cd gdextension-d # プロジェクトディレクトリに移動
$ dub init -n
     Success created empty project in C:\Users\castalien_naka\gdextension-d
             Package successfully created in .

これでsource/app.dというファイルが勝手に生成されています。C/C++やC#やJavaScriptを使ったことのある人なら見ればなんとなく分かる感じかと思います。

source/app.d
import std.stdio;

void main()
{
	writeln("Edit source/app.d to start your project.");
}

dub runでビルドおよび実行がされます。

$ dub run
    Starting Performing "debug" build using C:\D\dmd2\windows\bin64\dmd.exe for x86_64.
    Building gdextension-d ~master: building configuration [application]
     Linking gdextension-d
     Running gdextension-d.exe
Edit source/app.d to start your project. 

DLLを作る

GDExtensionを作るためには共有ライブラリ(DLL)を作る必要があります。まだ通常の実行ファイルなので、これを共有ライブラリに変更します。

dubのビルド設定ファイルdub.jsonを編集すると、DLLを作るように修正できます。

dub.json
{
	"authors": [
		"castalien_naka"
	],
	"description": "A minimal D application.",
	"license": "BSL-1.0",
	"name": "gdextension-d",
	"targetType": "dynamicLibrary"
}

"targetType": "dynamicLibrary"が修正箇所になります。
さらに、Windowsでは多少のコード修正が必要になります。

source/app.d
// Windows向けの初期化関数等を追加。このあたりはD言語の機能をバリバリ使ってます。
// 今はおまじないだと思ってください!
version(Windows)
{
	import core.sys.windows.dll : SimpleDllMain;

	mixin SimpleDllMain;
}

// mainは消して、DLLとしてのエントリーポイントになる関数を追加
// exportによりDLLで公開される関数となる。
// extern(C)は関数の呼び出し規約の指定。
export extern(C) void entryPoint()
{
    // まだなにもしない
}

上記まで修正してdub buildすると、それだけでDLLであるgdextension-d.dllができます。

$ dub build
    Starting Performing "debug" build using C:\D\dmd2\windows\bin64\dmd.exe for x86_64.
    Building gdextension-d ~master: building configuration [library]
     Linking gdextension-d

D言語でDLLを作る手順は以上です。割と簡単だな~と思ってもらえたら嬉しいです。

何もしないGDExtensionの実装

さて、ようやくGodotの話になります。まずは何もしないGDExtensionを作って様子を見てみましょう。

Godot実行ファイルによるC言語ヘッダ・クラス情報JSONの生成

Godotの実行ファイルにはGDExtension用のインターフェイス情報(C言語向けヘッダファイル・クラス情報のJSONファイル)を出力する機能があります。

以下のようにオプションを付けてGodotを実行することで、それぞれのファイルが出力されます。

$ .\Godot_v4.3-stable_mono_win64.exe --dump-gdextension-interface --dump-extension-api

# Godotが起動してすぐ終わる
# extension_api.json・gdextension_interface.hがカレントディレクトリに生成されている。
  • gdextension_interface.h
    • GDExtension自体の機能(初期化・組み込み型の値の生成・ClassDBの使用など)を提供するC言語のヘッダファイル
  • extension_api.json
    • Godotの各クラス(Vector2やNodeやSprite2Dなど)の情報

この2つのファイルを駆使してGDExtensionを利用していくことになります。
各種バインディングライブラリは上記のファイルを元にコードを自動生成したりして頑張っているのですね。(たぶん)

gdextension_interface.hを見れば分かる通り、内容としてはただのC言語の構造体・関数の集合になっています。依存ライブラリ等もほぼありません。このC言語の関数さえ正しく使えれば、GDExtensionとしての機能は実現できることになります。

そんなわけでgdextension_interface.hがある意味GDExtensionの全てなのですが、意外とこの内容についての情報が無く……gdextension_interface.hやGodot自体のソースコードをひたすら精読する生活になります。

GDExtensionの実行の流れ

gdextension_interface.hに書いてある内容を元に処理の流れを書いておきます。
GDExtensionは以下の順序で実行されます。

  1. Godot起動時(?)、GDExtensionの設定ファイルXXX.gdextensionentry_symbolに書いてあるDLLの関数が呼び出される。引数にはGodotのAPI関数取得用の関数ポインタ・クラスライブラリポインタ・初期化情報の書き込み先ポインタが渡される。
    • 具体的にはGDExtensionInitializationFunctionとしてシグネチャが定義されています。起動時に行うこともそのコメントに書いてあります。
  2. GDExtensionの側でDLL関数の引数を使って使用するAPI関数を取得し、初期化情報を書き込む。また、GDExtension全体での初期化処理があれば実行する。そこまででDLL関数は一旦終了する。
  3. GodotはGDExtensionにより設定された初期化情報を元に、GDExtensionの初期化(initialize)・後処理(deinitialize)を指定されたタイミングで呼び出す。
    • GDExtensionの初期化処理を呼び出すタイミングはGDExtensionInitializationLevelというenumで定義されていて、コア起動時・サーバー起動時・シーン・エディタといったどのレベルで初期化・後処理を行うかを指定できます。

上記のように初期化・後処理が呼び出されるので、基本的にはその処理の中でClassDBへのクラスやメソッドの登録を行い、Godotのゲーム中でそれを利用します。
GDExtensionで登録したクラスのメソッドを呼び出したり生成・破棄を行うことで、GDExtensionの機能が利用できることになります。

GDExtension用インターフェイスの型定義をポーティングする

なんとなく大まかな流れが分かったところで、最初にやることは、GDExtensionのための構造体や関数の定義のポーティング(D言語側への取り込み)です。

C/C++なら不要なのに……。

実はD言語にはImportCという機能があって、頑張ればこの作業も無くせるかもしれませんが、これはこれでプリプロセス等の準備が必要なので、今回は一番原始的な方法にします。

また、必要分だけポーティングして使うというのは、依存性の管理や内容の把握という点ではメリットもあります。

さて、sourceディレクトリにgdextension_interface.dというソースを用意して、そちらにgdextension_interface.hの必要な内容をD向けに書き写していくことにします。

gdextension_interface.d
/**
モジュール宣言。他ファイルでimportするときにこの名前を使う。
*/
module gdextension_interface;

// とりあえず必要なものだけimport
import std.stdint : uint8_t;

// これ以降の関数ポインタ等は、
// C言語の呼び出し規約に従い、外部のライブラリの関数を指すシステム提供のものになる
// という宣言
extern(C) @system:

// DのaliasはC/C++におけるtypedef
alias GDExtensionBool = uint8_t;
alias GDExtensionClassLibraryPtr = void*;

enum GDExtensionInitializationLevel
{
	GDEXTENSION_INITIALIZATION_CORE,
	GDEXTENSION_INITIALIZATION_SERVERS,
	GDEXTENSION_INITIALIZATION_SCENE,
	GDEXTENSION_INITIALIZATION_EDITOR,
	GDEXTENSION_MAX_INITIALIZATION_LEVEL,
}

struct GDExtensionInitialization
{
	GDExtensionInitializationLevel minimum_initialization_level;
	void* userdata;
	void function(void* userdata, GDExtensionInitializationLevel p_level) initialize;
	void function(void* userdata, GDExtensionInitializationLevel p_level) deinitialize;
}

// 関数ポインタの型定義
alias GDExtensionInterfaceFunctionPtr = void function();
alias GDExtensionInterfaceGetProcAddress = GDExtensionInterfaceFunctionPtr function(
	const(char)* p_function_name) @nogc nothrow; // @nogc nothrowはGCも例外も使わない関数であることを示す

gdextension_interface.hと見比べると、かなり近い、ほぼそのままな感じでいけるなと思ってもらえると嬉しいなといった所感です。これがD言語がD言語を名乗る所以ですね。

GDExtension用エントリーポイントを実装する

上記の先ほどのgdextension_interface.dapp.dimportして使います。

app.d
/**
こちらも一応module宣言
*/
module app;

// 先ほどのgdextension_interface.dをimport
// 全体的に使用するので選択importではなくする
// この形式であればgdextension_interface.dで公開されているすべてのシンボルが使える
import gdextension_interface;

// ... おまじないSimpleDllMain部分 ...

/**
GDExtensionのエントリーポイント。

@param p_get_proc_address インターフェイス関数取得用の関数のポインタ
@param p_library クラスライブラリのポインタ。後でClassDBなどが使う
@param r_initialization 初期化情報
@return 何返せば良いのかあまり情報がないけどたぶんtrue返しておけば問題ないと思う……。
*/
pragma(mangle, "gdextension_d_entry_point") // DLLで公開する名前としてはgdextension_d_entry_pointにする、という指定
export extern(C) GDExtensionBool entryPoint(
    GDExtensionInterfaceGetProcAddress p_get_proc_address,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization* r_initialization
) nothrow
{
    // 最低限の初期化処理
    with (r_initialization)
	{
        initialize = &gdextensionInitialize;
        deinitialize = &gdextensionDeinitialize;
        userdata = null;

        // Godotのコア起動時・終了時に初期化・終了処理が行われるようにする。
        minimum_initialization_level = GDExtensionInitializationLevel.GDEXTENSION_INITIALIZATION_CORE;
	}
	return true;
}

/**
GDExtensionの初期化処理。
r_initialization.minimum_initialization_level で指定されたレベルまでの初期化が発生するたびに呼び出される点に注意。

@param userdata ユーザー用のデータ。r_initializationに設定したもの
@param p_level 初期化レベル。r_initializationで指定したレベルまでのどの段階の初期化かを示す。
*/
extern(C) void gdextensionInitialize(
    void* userdata,
    GDExtensionInitializationLevel p_level) nothrow
{
    // まだ何もしない
}

/**
GDExtensionの終了処理。
こちらもr_initialization.minimum_initialization_level で指定されたレベルまでの初期化が発生するたびに呼び出される点に注意。

@param userdata ユーザー用のデータ。r_initializationに設定したもの
@param p_level 初期化レベル。r_initializationで指定したレベルまでのどの段階の初期化かを示す。
*/
extern(C) void gdextensionDeinitialize(
    void* userdata,
    GDExtensionInitializationLevel p_level) nothrow
{
    // まだ何もしない
}

r_initializationを埋めただけで何もしないGDExtensionができました。dub buildでDLLが作れると思います。

これをまずGodotに使ってもらいます。

.gdextensionファイルの作成

実行の流れのところで書いたように、GDExtensionをGodotで利用するためにはDLLに加えてGDExtensionの設定ファイル.gdextensionが必要になります。

今回はgdextension-d.gdextensionというファイル名で作ります。

gdextension-d.gdextension
[configuration]

; エントリーポイントとする関数の公開名
entry_symbol = "gdextension_d_entry_point"

; 対応するGodotバージョン。
compatibility_minimum = "4.3"

[libraries]

; 使うDLLのファイル名。プラットフォーム別に指定可能
linux.64 = "gdextension-d.so"
windows.64 = "gdextension-d.dll"

これを見ると、1つのDLLに複数のエントリーポイントを持たせて、複数のGDExtensionとして扱うこともできそうですね。

何もしないGDExtensionのGodotプロジェクトへの取り込みと実行

DLLのビルドが成功し、.gdextensionファイルが用意出来たら、いよいよGodotのプロジェクトへの取り込みです。

これまた最初の何もないプロジェクトexamplegdextension-dの中に作り、とりあえずメインシーンmain.trsnでも作っておいて、ゲームを起動できるようにしておきます。

そして、exampleの中に作ったGDExtensionを置けるディレクトリgdextension-dを掘り、その中にDLL・.gdextensionファイル、ついでにpdbファイルも使えるかもしれないので置いておきます。

exampleのgdextension-dの中に各ファイルを配置

そしてGodot Editorを起動します。

gdextension-dは見えるが特にログ等は出ない

うーん、特にエラー等も無いし取り込めているっぽいですが、本当に動いているのか全然分からないですね。

せめてメッセージを出す

せめてターミナルにメッセージくらいは出したいので、ポーティング関数を追加します。

print_warning_with_messageという関数があって、これで警告メッセージが出せるので、この関数を使えるようにしてみます。

まずは型定義です。

gdextension_interface.d
// ...

// int_32_tを追加
import std.stdint : uint8_t, int32_t;

// ...

// print_warning_with_messageの関数シグネチャを表す型を追加
alias GDExtensionInterfacePrintWarningWithMessage = void function(
    const(char)* p_description,
    const(char)* p_message,
    const(char)* p_function,
    const(char)* p_file,
    int32_t p_line,
    GDExtensionBool p_editor_notify
) @nogc nothrow;

gdextension_interface.hの関数は静的にリンクできるわけではなくて、GDExtension側でp_get_proc_addressを使って実行時に取得する必要があります。

とりあえずグローバル変数にprint_warning_with_messageを用意して、エントリーポイントで関数ポインタを取得して格納するようにしてみましょう。

app.d
// ...

pragma(mangle, "gdextension_d_entry_point") // DLLで公開する名前としてはgdextension_d_entry_pointにする、という指定
export extern(C) GDExtensionBool entryPoint(
    GDExtensionInterfaceGetProcAddress p_get_proc_address,
    GDExtensionClassLibraryPtr p_library,
    GDExtensionInitialization* r_initialization
) nothrow
{
    with (r_initialization)
    {
        initialize = &gdextensionInitialize;
        deinitialize = &gdextensionDeinitialize;
        userdata = null;

        minimum_initialization_level = GDExtensionInitializationLevel.GDEXTENSION_INITIALIZATION_CORE;
    }

    // 関数名を指定して関数ポインタを取得。
    // グローバル変数の定義は後回し(D言語は後方参照OK)
    // Dの文字列リテラルはちゃんとnull終端されるのでそのままC言語文字列として渡しても大丈夫
    // castはD言語のキャスト
    print_warning_with_message = cast(GDExtensionInterfacePrintWarningWithMessage)
        p_get_proc_address("print_warning_with_message");

    // 取得できた先から呼び出してみる。
    print_warning_with_message(
        "GDExtension-D",
        "Hello, GDExtension-D!",
        __FUNCTION__,
        __FILE__,
        __LINE__,
        true);

	return true;
}

// 関数ポインタの格納先のグローバル変数
// Dのグローバル変数は実はデフォルトでスレッドローカルになるので、念のため__gsharedを付けて本当のグローバル変数にしておく。
__gshared GDExtensionInterfacePrintWarningWithMessage print_warning_with_message;

これでビルドして、Godot側のファイルを更新して動かしてみると……。

Godot Editorには特に変化がない

特に変わっていないみたいですね。エラーも出ていませんが。

実は、エントリーポイント時のprint系関数の出力は、コマンドプロンプトのコンソールの方に出ています。
コマンドプロンプトから実行してみると、たしかにメッセージが出ています!

$ .\Godot_v4.3-stable_mono_win64.exe C:\Users\castalien_naka\gdextension-d\example\project.godot
WARNING: Hello, GDExtension-D!
     at: (source\app.d:48)
Godot Engine v4.3.stable.mono.official.77dcf97d8 - https://godotengine.org
OpenGL API 3.3.0 - Build 30.0.101.1371 - Compatibility - Using Device: Intel - Intel(R) UHD Graphics 730

Hello, GDExtension-D!って出てる!
これでようやくGDExtensionの起動が確認できました。

次回予告

さて、最低限のGDExtension向けのDLLをバインディングライブラリ無しで作る手順を見てきました。
正直これどこまで需要あるか分かりませんが……。

とはいえ、意外とGodotと外部ライブラリを少ない依存関係で結びたい場合は結構多いんじゃないかなと思って、記事にしてみました。
少なくとも自分の好きな言語でバインディングライブラリ作ろうとするときには必要になる情報です。

これからさらに、Godotのユーティリティ関数や新たなクラスの定義等が行えるまでやってみようと思います。コードもGithubで公開予定です。

Discussion