diff --git a/src/test-finalizers.cpp b/src/test-finalizers.cpp new file mode 100644 index 0000000..488f9e4 --- /dev/null +++ b/src/test-finalizers.cpp @@ -0,0 +1,70 @@ +#ifdef LOCAL_TEST +#include "delegate.hpp" +#include "macros.hpp" + +DECLARE_CLASS(FinalizerTests, Plain, "System", "Object", sizeof(Il2CppObject)){}; +DEFINE_TYPE(FinalizerTests, Plain); + +DECLARE_CLASS(FinalizerTests, Named, "System", "Object", sizeof(Il2CppObject)) { + DECLARE_DTOR(Finalize); +}; +DEFINE_TYPE(FinalizerTests, Named); +void FinalizerTests::Named::Finalize() { + this->~Named(); +} + +DECLARE_CLASS(FinalizerTests, Renamed, "System", "Object", sizeof(Il2CppObject)) { + DECLARE_DTOR(Cleanup); +}; +DEFINE_TYPE(FinalizerTests, Renamed); +void FinalizerTests::Renamed::Cleanup() { + this->~Renamed(); +} + +DECLARE_CLASS(FinalizerTests, Simple, "System", "Object", sizeof(Il2CppObject)) { + DECLARE_SIMPLE_DTOR(); +}; +DEFINE_TYPE(FinalizerTests, Simple); + +DECLARE_CLASS_CUSTOM(FinalizerTests, Inherited, FinalizerTests::Renamed){}; +DEFINE_TYPE(FinalizerTests, Inherited); + +DECLARE_CLASS_CUSTOM(FinalizerTests, OverrideInherited, FinalizerTests::Renamed) { + DECLARE_OVERRIDE_METHOD(void, CleanupAgain, i2c::find_method(i2c::class_of(), {"Cleanup", 0})); +}; +DEFINE_TYPE(FinalizerTests, OverrideInherited); +void FinalizerTests::OverrideInherited::CleanupAgain() { + this->~OverrideInherited(); +} + +// Merely having a method named Finalize must not opt a class into finalization. +DECLARE_CLASS(FinalizerTests, NonVirtual, "System", "Object", sizeof(Il2CppObject)) { + DECLARE_INSTANCE_METHOD(void, Finalize); +}; +DEFINE_TYPE(FinalizerTests, NonVirtual); +void FinalizerTests::NonVirtual::Finalize() {} + +namespace { + void CheckFinalizer(Il2CppClass* klass, bool expected) { + if (!klass || bool(klass->has_finalize) != expected) { + custom_types::logger.critical("Finalizer regression: {} expected has_finalize={}", klass ? klass->name : "", expected); + SAFE_ABORT(); + } + } +} + +// Invoke explicitly after AutoRegister in a LOCAL_TEST build. Referencing the +// delegate registration objects also ensures both template types are registered. +void testFinalizers() { + CheckFinalizer(i2c::class_of(), false); + CheckFinalizer(i2c::class_of(), true); + CheckFinalizer(i2c::class_of(), true); + CheckFinalizer(i2c::class_of(), true); + CheckFinalizer(i2c::class_of(), true); + CheckFinalizer(i2c::class_of(), true); + CheckFinalizer(i2c::class_of(), false); + CheckFinalizer(custom_types::DelegateWrapperStatic::__registration_instance_DelegateWrapperStatic.klass(), true); + CheckFinalizer(custom_types::DelegateWrapperInstance::__registration_instance_DelegateWrapperInstance.klass(), true); + custom_types::logger.info("Finalizer registration: all 9 regression cases passed"); +} +#endif diff --git a/src/type-registration.cpp b/src/type-registration.cpp index 44dda45..553b114 100644 --- a/src/type-registration.cpp +++ b/src/type-registration.cpp @@ -1,3 +1,4 @@ +#include "beatsaber-hook/shared/find.hpp" #include "extra-typedefs.hpp" #include "logging.hpp" #include "register.hpp" @@ -610,8 +611,10 @@ namespace custom_types { for (size_t i = 0; i < offsets.size(); i++) { k->interfaceOffsets[i] = offsets[i]; } - // If any method exists that has a Finalize name, should use it - k->has_finalize = false; + // Finalization follows the System.Object.Finalize slot, regardless of the + // override's name. The completed vtable also includes inherited finalizers. + static auto* const objectFinalize = THROW_UNLESS(logger, i2c::find_method({"System", "Object"}, {"Finalize", 0})); + k->has_finalize = k->vtable[objectFinalize->slot].method != objectFinalize; // TODO: Allow cctor to exist someday k->has_cctor = false; @@ -623,9 +626,6 @@ namespace custom_types { m->get()->klass = k; // TODO: Populate other fields as necessary k->methods[i] = m->get(); - if (!k->has_finalize && checkVirtualsForMatch(methods[i], "System", "Object", "Finalize", 0)) { - k->has_finalize = true; - } } }