【Godot4】[if node] ≠ [if node ! = null]の謎
0. この記事の内容
GDScriptのif
文でのnull
チェック時の少し奇妙な挙動について調べたのでその備忘録としてまとめました。これが原因でクラッシュなんてこともあるので、参考にしてください。
Godot 4.3-stable
を使用しています。ソースコードは2024年9月8日に参照したものです。個人的にコードブロックの方が見やすいので該当箇所を抜き出したコードブロックとGitHubへのリンクを併記しています。
if node
の奇妙な挙動
1. 突然ですが、次のメソッドを実行したらどのように出力されるでしょうか?
func test() -> void:
var node: Node = null
print("null , if node != null : ", true if node != null else false)
print("null , if node : ", true if node else false)
node = Node.new()
node.queue_free()
# freeされるのを待つためにprocess_frameをawait
await get_tree().process_frame
print("freed, if node != null : ", true if node != null else false)
print("freed, if node : ", true if node else false)
答えはこちら
null , if node != null : false
null , if node : false
freed, if node != null : false
freed, if node : true
破棄されたオブジェクトのif node
だけがtrue
となりました。
予想は当たっていたでしょうか?
if node
がtrue
になるのか
2. なぜなぜこのような現象が起きるのでしょう?幸いGodotはオープンソースなゲームエンジンです。中身を覗いてみましょう。
GDScriptを解釈する処理はgdscript_vm.cpp
にあります。その中のif
文の条件式に関する処理を見てみましょう。
bool result = test->booleanize();
gdscript_vm.cpp
どうやらif
の条件式はbooleanize
によってbool
に変換されているようです。booleanize
の定義されているvariant_op.cpp
を調べると、その中ではis_zero
が呼ばれています。
bool Variant::booleanize() const {
return !is_zero();
}
variant_op.cpp
次はis_zero
の定義されているvariant.cpp
を見てみましょう。
今回用いたnode
はObject
を継承しているので953行~が該当します。
case OBJECT: {
return _get_obj().obj == nullptr;
}
variant.cpp
is_zero
はオブジェクトがnullptr
の際はtrue
を返すことが分かります。
オブジェクトを破棄した後もif node
がtrue
になったということは、破棄後そのオブジェクトの参照はnullptr
にならないということです。破棄された後も同じアドレスを保持し続けるため、メモリアクセス違反を起こしてしまうのです。
C
やC++
を使っていれば当たり前っちゃ当たり前なのですが、GDScriptのようなリッチな言語を使っているとどうしても意識が薄くなりがちです。
if node != null
がfalse
になるのか
3. なぜif node
がtrue
になった謎については解決しましたが、ではなぜif node != null
はfalse
になったのでしょうか。こちらも調べてみましょう。
演算子の実装はvariant_op.h
に書かれています。
static void evaluate(const Variant &p_left, const Variant &p_right, Variant *r_ret, bool &r_valid) {
const Object *a = p_left.get_validated_object();
const Object *b = p_right.get_validated_object();
*r_ret = a == b;
r_valid = true;
}
variant_op.h
等価演算子の評価の過程で、オブジェクトの参照はget_validated_object
を用いて取得されています。get_validated_object
はvariant.cpp
に定義されています。
Object *Variant::get_validated_object() const {
if (type == OBJECT) {
return ObjectDB::get_instance(_get_obj().id);
} else {
return nullptr;
}
}
variant.cpp
get_validated_object
ではget_instance
を用いて参照を取得しています。get_instance
はobject.h
のObjectDB
クラスに定義されています。
if (unlikely(object_slots[slot].validator != validator)) {
spin_lock.unlock();
return nullptr;
}
variant.cpp
get_instance
は破棄されている等、オブジェクトが有効でない場合はnullptr
を返すようになっています。そのため破棄されたオブジェクトに対してif node != null
はfalse
になったのです。
4. バグでした
「この挙動はバグではないか?」と思い調べたところ、やはりバグでした。
しかしなかなか根深いバグで、簡単には修正できないようです。
少し面倒くさいですが、オブジェクトが有効かどうかをチェックしたい場合はis_instance_valid
を使うのが良さそうです。
5. おわり
ふと疑問に思った奇妙な挙動について調べました。今回はコードを追い、オブジェクト参照の仕方の違いによってif node
とif node != null
は異なる結果になっていると結論づけました。しかし、バグ修正が難航していることから、もしかしたらそんな単純な問題では無いのかもしれません。間違いがあればご指摘お願いします。
Discussion