From fd15fb53245172579e570af433c892074434d1e5 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 20:44:10 -0500 Subject: [PATCH 1/4] Add shared IL2CPP runtime boundary helpers with contract tests --- .github/workflows/ci.yaml | 12 +++ mods/src/il2cpp/il2cpp_helper.h | 6 +- mods/src/il2cpp/runtime.h | 59 +++++++++++++++ tests/il2cpp_runtime.cc | 127 ++++++++++++++++++++++++++++++++ xmake.lua | 11 +++ 5 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 mods/src/il2cpp/runtime.h create mode 100644 tests/il2cpp_runtime.cc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 61cf42826..ff6de6ff7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -189,6 +189,13 @@ jobs: shell: pwsh run: sccache --zero-stats + - name: Test IL2CPP runtime helpers + run: | + xmake build -y il2cpp-runtime-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + xmake run il2cpp-runtime-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Build run: xmake build -y stfc-community-mod @@ -469,6 +476,11 @@ jobs: shell: bash run: sccache --zero-stats + - name: Test IL2CPP runtime helpers + run: | + xmake build -y il2cpp-runtime-tests + xmake run il2cpp-runtime-tests + - name: Build # -D keeps the existing XMake compiler-cache diagnostics too. run: xmake -y -D diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 3d4ff6e3f..19708b2cd 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); - auto image = il2cpp_assembly_get_image(assemblyT); + auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; + auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; - auto cls = il2cpp_class_from_name(image, namespacez, name); + auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; return IL2CppClassHelper{cls}; } diff --git a/mods/src/il2cpp/runtime.h b/mods/src/il2cpp/runtime.h new file mode 100644 index 000000000..8617cedb1 --- /dev/null +++ b/mods/src/il2cpp/runtime.h @@ -0,0 +1,59 @@ +#pragma once + +#include "il2cpp_helper.h" +#include + +// Mechanics shared by optional runtime features. Signature/overload selection, +// argument storage and feature-specific failure policy remain with the caller. +namespace Il2CppRuntime +{ +inline Il2CppClass* Class(const char* assembly, const char* ns, const char* name) +{ return il2cpp_get_class_helper(assembly, ns, name).get_cls(); } + +inline const MethodInfo* Method(Il2CppClass* cls, const char* name, int count) +{ return IL2CppClassHelper(cls).GetMethodInfo(name, count); } + +inline bool Type(const Il2CppType* type, int expected) +{ return type && !type->byref && type->type == expected; } + +inline bool Reference(const Il2CppType* type) +{ + return Type(type, IL2CPP_TYPE_CLASS) || Type(type, IL2CPP_TYPE_GENERICINST) || Type(type, IL2CPP_TYPE_OBJECT) + || Type(type, IL2CPP_TYPE_STRING); +} + +inline bool Instance(const MethodInfo* method, int count, int result) +{ + return method && method->methodPointer && method->invoker_method && !(method->flags & METHOD_ATTRIBUTE_STATIC) + && method->parameters_count == count && Type(method->return_type, result) + && !method->has_full_generic_sharing_signature; +} + +// The method and argument ABI must already be established by the caller. +// IL2CPP takes references directly, but value/byref arguments as addresses. +// A null target is valid for static methods. Success is independent of whether +// the return value is null (including void methods). Outputs change on success only. +inline bool TryInvoke(const MethodInfo* method, void* target, void** args, Il2CppObject** result = nullptr) +{ + if (!method) + return false; + Il2CppException* exception = nullptr; + auto* value = il2cpp_runtime_invoke(method, target, args, &exception); + if (exception) + return false; + if (result) + *result = value; + return true; +} + +inline bool TryBoolean(Il2CppObject* boxed, bool& value) +{ + if (!boxed || !boxed->klass || !Type(il2cpp_class_get_type(boxed->klass), IL2CPP_TYPE_BOOLEAN)) + return false; + auto* data = static_cast(il2cpp_object_unbox(boxed)); + if (!data) + return false; + value = *data; + return true; +} +} // namespace Il2CppRuntime diff --git a/tests/il2cpp_runtime.cc b/tests/il2cpp_runtime.cc new file mode 100644 index 000000000..1b7113340 --- /dev/null +++ b/tests/il2cpp_runtime.cc @@ -0,0 +1,127 @@ +// Production helpers with a controlled IL2CPP boundary, on Windows and macOS. +#include +#if _WIN32 +#undef IL2CPP_IMPORT +#define IL2CPP_IMPORT +#endif +#include "il2cpp/runtime.h" +#include +#include +#include + +namespace +{ +Il2CppClass klass; +Il2CppType type{}; +Il2CppObject object{}; +MethodInfo method{}; +int stage = 3, calls = 0; +bool fail = false, boxed = false, null_result = false; +void* seen_target = nullptr; +void** seen_args = nullptr; +} // namespace +#if _WIN32 +#define API(ret, name, params) extern "C" ret name params +#define END_API +#else +#define API(ret, name, params) name##_t name = +[] params->ret +#define END_API ; +#endif +API(Il2CppDomain*, il2cpp_domain_get, ()) +{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } +END_API +API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) +{ + if (!domain) + std::abort(); + return stage >= 1 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) +{ + if (!assembly) + std::abort(); + return stage >= 2 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) +{ + if (!image) + std::abort(); + return stage >= 3 ? &klass : nullptr; +} +END_API +API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) +{ + if (!cls) + std::abort(); + return &method; +} +END_API +API(const Il2CppType*, il2cpp_class_get_type, (Il2CppClass*)) +{ return &type; } +END_API +API(void*, il2cpp_object_unbox, (Il2CppObject*)) +{ return &boxed; } +END_API +API(Il2CppObject*, il2cpp_runtime_invoke, (const MethodInfo*, void* target, void** args, Il2CppException** error)) +{ + ++calls; + seen_target = target; + seen_args = args; + if (fail) + *error = reinterpret_cast(&object); + return null_result ? nullptr : &object; +} +END_API +void Require(bool condition) +{ + if (!condition) + throw std::runtime_error("runtime helper regression"); +} +int main() +{ + for (stage = -1; stage < 3; ++stage) + Require(!Il2CppRuntime::Class("Assembly", "Namespace", "Class")); + Require(Il2CppRuntime::Class("Assembly", "Namespace", "Class") == &klass); + Require(!Il2CppRuntime::Method(nullptr, "Method", 0)); + Require(Il2CppRuntime::Method(&klass, "Method", 0) == &method); + + Il2CppObject* result = &object; + Require(!Il2CppRuntime::TryInvoke(nullptr, nullptr, nullptr, &result) && calls == 0 && result == &object); + bool value = true; + void* args[] = {&value, &object, nullptr}; + Require(Il2CppRuntime::TryInvoke(&method, &object, args, &result)); + Require(seen_target == &object && seen_args == args && seen_args[0] == &value && seen_args[1] == &object); + // Static calls, null reference/void returns and exceptions remain distinct. + null_result = true; + Require(Il2CppRuntime::TryInvoke(&method, nullptr, args, &result) && !result && !seen_target); + result = &object; + fail = true; + Require(!Il2CppRuntime::TryInvoke(&method, nullptr, args, &result) && result == &object); + + object.klass = &klass; + type.type = IL2CPP_TYPE_BOOLEAN; + Require(Il2CppRuntime::TryBoolean(&object, value) && !value); + boxed = true; + Require(Il2CppRuntime::TryBoolean(&object, value) && value); + type.type = IL2CPP_TYPE_I4; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + Require(!Il2CppRuntime::TryBoolean(nullptr, value)); + type.type = IL2CPP_TYPE_CLASS; + Require(Il2CppRuntime::Reference(&type)); + type.byref = true; + Require(!Il2CppRuntime::Reference(&type)); + type.byref = false; + type.type = IL2CPP_TYPE_VOID; + method.methodPointer = reinterpret_cast(1); + method.invoker_method = reinterpret_cast(1); + method.return_type = &type; + Require(Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + method.flags = METHOD_ATTRIBUTE_STATIC; + Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + method.flags = 0; + method.has_full_generic_sharing_signature = true; + Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + std::cout << "IL2CPP runtime helper regressions passed\n"; +} diff --git a/xmake.lua b/xmake.lua index 9b25acbfb..ac08005a2 100644 --- a/xmake.lua +++ b/xmake.lua @@ -26,3 +26,14 @@ add_rules("mode.releasedbg") includes("xmake/rules/protobuf_sccache.lua") includes("xmake/rules/cxx_sccache.lua") includes("mods") + +target("il2cpp-runtime-tests") + set_kind("binary") + set_default(false) + add_files("tests/il2cpp_runtime.cc") + add_includedirs("mods/src") + add_packages("libil2cpp", "eastl") + set_exceptions("cxx") + if is_plat("windows") then + add_linkdirs("mods/src/il2cpp") + end From 733997cfe2cd34c9203b61cff755046ca9937643 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 22:27:13 -0500 Subject: [PATCH 2/4] Narrow shared invocation helpers around existing loading screens --- mods/src/il2cpp/il2cpp_helper.h | 6 +- mods/src/il2cpp/runtime.h | 27 ++----- .../src/patches/parts/loading_screen_common.h | 16 ++--- tests/il2cpp_runtime.cc | 70 ++++--------------- 4 files changed, 28 insertions(+), 91 deletions(-) diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 19708b2cd..3d4ff6e3f 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; - auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; + auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); + auto image = il2cpp_assembly_get_image(assemblyT); - auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; + auto cls = il2cpp_class_from_name(image, namespacez, name); return IL2CppClassHelper{cls}; } diff --git a/mods/src/il2cpp/runtime.h b/mods/src/il2cpp/runtime.h index 8617cedb1..b3043673e 100644 --- a/mods/src/il2cpp/runtime.h +++ b/mods/src/il2cpp/runtime.h @@ -7,28 +7,6 @@ // argument storage and feature-specific failure policy remain with the caller. namespace Il2CppRuntime { -inline Il2CppClass* Class(const char* assembly, const char* ns, const char* name) -{ return il2cpp_get_class_helper(assembly, ns, name).get_cls(); } - -inline const MethodInfo* Method(Il2CppClass* cls, const char* name, int count) -{ return IL2CppClassHelper(cls).GetMethodInfo(name, count); } - -inline bool Type(const Il2CppType* type, int expected) -{ return type && !type->byref && type->type == expected; } - -inline bool Reference(const Il2CppType* type) -{ - return Type(type, IL2CPP_TYPE_CLASS) || Type(type, IL2CPP_TYPE_GENERICINST) || Type(type, IL2CPP_TYPE_OBJECT) - || Type(type, IL2CPP_TYPE_STRING); -} - -inline bool Instance(const MethodInfo* method, int count, int result) -{ - return method && method->methodPointer && method->invoker_method && !(method->flags & METHOD_ATTRIBUTE_STATIC) - && method->parameters_count == count && Type(method->return_type, result) - && !method->has_full_generic_sharing_signature; -} - // The method and argument ABI must already be established by the caller. // IL2CPP takes references directly, but value/byref arguments as addresses. // A null target is valid for static methods. Success is independent of whether @@ -48,7 +26,10 @@ inline bool TryInvoke(const MethodInfo* method, void* target, void** args, Il2Cp inline bool TryBoolean(Il2CppObject* boxed, bool& value) { - if (!boxed || !boxed->klass || !Type(il2cpp_class_get_type(boxed->klass), IL2CPP_TYPE_BOOLEAN)) + if (!boxed || !boxed->klass) + return false; + const auto* type = il2cpp_class_get_type(boxed->klass); + if (!type || type->byref || type->type != IL2CPP_TYPE_BOOLEAN) return false; auto* data = static_cast(il2cpp_object_unbox(boxed)); if (!data) diff --git a/mods/src/patches/parts/loading_screen_common.h b/mods/src/patches/parts/loading_screen_common.h index 071b63976..4f78890c7 100644 --- a/mods/src/patches/parts/loading_screen_common.h +++ b/mods/src/patches/parts/loading_screen_common.h @@ -3,7 +3,7 @@ #include "config.h" #include "errormsg.h" -#include +#include #include #include @@ -28,9 +28,8 @@ struct FakeColor { float r, g, b, a; }; inline Il2CppObject* InvokeRuntime(const MethodInfo* method, void* target, void** args, const char* name) { if (!method) return nullptr; - Il2CppException* exception = nullptr; - Il2CppObject* result = il2cpp_runtime_invoke(method, target, args, &exception); - if (exception) { + Il2CppObject* result = nullptr; + if (!Il2CppRuntime::TryInvoke(method, target, args, &result)) { spdlog::warn("[LS] {} invocation failed", name); return nullptr; } @@ -40,9 +39,7 @@ inline Il2CppObject* InvokeRuntime(const MethodInfo* method, void* target, void* inline bool InvokeVoid(const MethodInfo* method, void* target, void** args, const char* name) { if (!method) return false; - Il2CppException* exception = nullptr; - il2cpp_runtime_invoke(method, target, args, &exception); - if (exception) { + if (!Il2CppRuntime::TryInvoke(method, target, args)) { spdlog::warn("[LS] {} invocation failed", name); return false; } @@ -52,9 +49,8 @@ inline bool InvokeVoid(const MethodInfo* method, void* target, void** args, cons inline bool InvokeBool(const MethodInfo* method, void* target, void** args, const char* name) { Il2CppObject* result = InvokeRuntime(method, target, args, name); - if (!result) return false; - void* value = il2cpp_object_unbox(result); - return value ? *reinterpret_cast(value) : false; + bool value = false; + return Il2CppRuntime::TryBoolean(result, value) && value; } inline int32_t InvokeInt32(const MethodInfo* method, void* target, int32_t fallback, const char* name) diff --git a/tests/il2cpp_runtime.cc b/tests/il2cpp_runtime.cc index 1b7113340..b098a2bbc 100644 --- a/tests/il2cpp_runtime.cc +++ b/tests/il2cpp_runtime.cc @@ -15,8 +15,8 @@ Il2CppClass klass; Il2CppType type{}; Il2CppObject object{}; MethodInfo method{}; -int stage = 3, calls = 0; -bool fail = false, boxed = false, null_result = false; +int calls = 0; +bool fail = false, boxed = false, null_result = false, null_type = false, null_unbox = false; void* seen_target = nullptr; void** seen_args = nullptr; } // namespace @@ -27,42 +27,11 @@ void** seen_args = nullptr; #define API(ret, name, params) name##_t name = +[] params->ret #define END_API ; #endif -API(Il2CppDomain*, il2cpp_domain_get, ()) -{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } -END_API -API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) -{ - if (!domain) - std::abort(); - return stage >= 1 ? reinterpret_cast(&object) : nullptr; -} -END_API -API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) -{ - if (!assembly) - std::abort(); - return stage >= 2 ? reinterpret_cast(&object) : nullptr; -} -END_API -API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) -{ - if (!image) - std::abort(); - return stage >= 3 ? &klass : nullptr; -} -END_API -API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) -{ - if (!cls) - std::abort(); - return &method; -} -END_API API(const Il2CppType*, il2cpp_class_get_type, (Il2CppClass*)) -{ return &type; } +{ return null_type ? nullptr : &type; } END_API API(void*, il2cpp_object_unbox, (Il2CppObject*)) -{ return &boxed; } +{ return null_unbox ? nullptr : &boxed; } END_API API(Il2CppObject*, il2cpp_runtime_invoke, (const MethodInfo*, void* target, void** args, Il2CppException** error)) { @@ -81,12 +50,6 @@ void Require(bool condition) } int main() { - for (stage = -1; stage < 3; ++stage) - Require(!Il2CppRuntime::Class("Assembly", "Namespace", "Class")); - Require(Il2CppRuntime::Class("Assembly", "Namespace", "Class") == &klass); - Require(!Il2CppRuntime::Method(nullptr, "Method", 0)); - Require(Il2CppRuntime::Method(&klass, "Method", 0) == &method); - Il2CppObject* result = &object; Require(!Il2CppRuntime::TryInvoke(nullptr, nullptr, nullptr, &result) && calls == 0 && result == &object); bool value = true; @@ -108,20 +71,17 @@ int main() type.type = IL2CPP_TYPE_I4; Require(!Il2CppRuntime::TryBoolean(&object, value) && value); Require(!Il2CppRuntime::TryBoolean(nullptr, value)); - type.type = IL2CPP_TYPE_CLASS; - Require(Il2CppRuntime::Reference(&type)); + type.type = IL2CPP_TYPE_BOOLEAN; type.byref = true; - Require(!Il2CppRuntime::Reference(&type)); - type.byref = false; - type.type = IL2CPP_TYPE_VOID; - method.methodPointer = reinterpret_cast(1); - method.invoker_method = reinterpret_cast(1); - method.return_type = &type; - Require(Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); - method.flags = METHOD_ATTRIBUTE_STATIC; - Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); - method.flags = 0; - method.has_full_generic_sharing_signature = true; - Require(!Il2CppRuntime::Instance(&method, 0, IL2CPP_TYPE_VOID)); + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + type.byref = false; + null_type = true; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + null_type = false; + null_unbox = true; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); + null_unbox = false; + object.klass = nullptr; + Require(!Il2CppRuntime::TryBoolean(&object, value) && value); std::cout << "IL2CPP runtime helper regressions passed\n"; } From d6c10b7d0d0988d4438c488cfa9354524d820106 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Mon, 21 Sep 2026 22:28:55 -0500 Subject: [PATCH 3/4] Guard existing IL2CPP class lookup when metadata is unavailable --- .github/workflows/ci.yaml | 12 ++++++ mods/src/il2cpp/il2cpp_helper.h | 6 +-- tests/il2cpp_class_lookup.cc | 71 +++++++++++++++++++++++++++++++++ xmake.lua | 11 +++++ 4 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 tests/il2cpp_class_lookup.cc diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 61cf42826..025c4356b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -189,6 +189,13 @@ jobs: shell: pwsh run: sccache --zero-stats + - name: Test IL2CPP class lookup + run: | + xmake build -y il2cpp-class-lookup-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + xmake run il2cpp-class-lookup-tests + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + - name: Build run: xmake build -y stfc-community-mod @@ -469,6 +476,11 @@ jobs: shell: bash run: sccache --zero-stats + - name: Test IL2CPP class lookup + run: | + xmake build -y il2cpp-class-lookup-tests + xmake run il2cpp-class-lookup-tests + - name: Build # -D keeps the existing XMake compiler-cache diagnostics too. run: xmake -y -D diff --git a/mods/src/il2cpp/il2cpp_helper.h b/mods/src/il2cpp/il2cpp_helper.h index 3d4ff6e3f..19708b2cd 100644 --- a/mods/src/il2cpp/il2cpp_helper.h +++ b/mods/src/il2cpp/il2cpp_helper.h @@ -337,10 +337,10 @@ class IL2CppClassHelper inline IL2CppClassHelper il2cpp_get_class_helper_impl(const char* assembly, const char* namespacez, const char* name) { auto domain = il2cpp_domain_get(); - auto assemblyT = il2cpp_domain_assembly_open(domain, assembly); - auto image = il2cpp_assembly_get_image(assemblyT); + auto assemblyT = domain ? il2cpp_domain_assembly_open(domain, assembly) : nullptr; + auto image = assemblyT ? il2cpp_assembly_get_image(assemblyT) : nullptr; - auto cls = il2cpp_class_from_name(image, namespacez, name); + auto cls = image ? il2cpp_class_from_name(image, namespacez, name) : nullptr; return IL2CppClassHelper{cls}; } diff --git a/tests/il2cpp_class_lookup.cc b/tests/il2cpp_class_lookup.cc new file mode 100644 index 000000000..0d471ea00 --- /dev/null +++ b/tests/il2cpp_class_lookup.cc @@ -0,0 +1,71 @@ +// Production helpers with a controlled IL2CPP boundary, on Windows and macOS. +#include +#if _WIN32 +#undef IL2CPP_IMPORT +#define IL2CPP_IMPORT +#endif +#include "il2cpp/il2cpp_helper.h" +#include +#include +#include + +namespace +{ +Il2CppClass klass; +Il2CppObject object{}; +MethodInfo method{}; +int stage = 3; +} // namespace +#if _WIN32 +#define API(ret, name, params) extern "C" ret name params +#define END_API +#else +#define API(ret, name, params) name##_t name = +[] params->ret +#define END_API ; +#endif +API(Il2CppDomain*, il2cpp_domain_get, ()) +{ return stage >= 0 ? reinterpret_cast(&object) : nullptr; } +END_API +API(const Il2CppAssembly*, il2cpp_domain_assembly_open, (Il2CppDomain * domain, const char*)) +{ + if (!domain) + std::abort(); + return stage >= 1 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(const Il2CppImage*, il2cpp_assembly_get_image, (const Il2CppAssembly* assembly)) +{ + if (!assembly) + std::abort(); + return stage >= 2 ? reinterpret_cast(&object) : nullptr; +} +END_API +API(Il2CppClass*, il2cpp_class_from_name, (const Il2CppImage* image, const char*, const char*)) +{ + if (!image) + std::abort(); + return stage >= 3 ? &klass : nullptr; +} +END_API +API(const MethodInfo*, il2cpp_class_get_method_from_name, (Il2CppClass * cls, const char*, int)) +{ + if (!cls) + std::abort(); + return &method; +} +END_API +void Require(bool condition) +{ + if (!condition) + throw std::runtime_error("class lookup regression"); +} +int main() +{ + for (stage = -1; stage < 3; ++stage) + Require(!il2cpp_get_class_helper("Assembly", "Namespace", "Class").get_cls()); + Require(il2cpp_get_class_helper("Assembly", "Namespace", "Class").get_cls() == &klass); + Require(!IL2CppClassHelper(nullptr).GetMethodInfo("Method", 0)); + Require(IL2CppClassHelper(&klass).GetMethodInfo("Method", 0) == &method); + + std::cout << "IL2CPP class lookup regressions passed\n"; +} diff --git a/xmake.lua b/xmake.lua index 9b25acbfb..cfbefafff 100644 --- a/xmake.lua +++ b/xmake.lua @@ -26,3 +26,14 @@ add_rules("mode.releasedbg") includes("xmake/rules/protobuf_sccache.lua") includes("xmake/rules/cxx_sccache.lua") includes("mods") + +target("il2cpp-class-lookup-tests") + set_kind("binary") + set_default(false) + add_files("tests/il2cpp_class_lookup.cc") + add_includedirs("mods/src") + add_packages("libil2cpp", "eastl") + set_exceptions("cxx") + if is_plat("windows") then + add_linkdirs("mods/src/il2cpp") + end From 454dae35513db8d70e40265f5ea6dbdbb019a579 Mon Sep 17 00:00:00 2001 From: Guffawaffle Date: Thu, 8 Oct 2026 19:37:27 -0500 Subject: [PATCH 4/4] Reuse checked Boolean results and identify settings failures --- .../parts/mod_confirmation_settings.cc | 143 ++++++++++++------ 1 file changed, 99 insertions(+), 44 deletions(-) diff --git a/mods/src/patches/parts/mod_confirmation_settings.cc b/mods/src/patches/parts/mod_confirmation_settings.cc index 3db918011..2610af62d 100644 --- a/mods/src/patches/parts/mod_confirmation_settings.cc +++ b/mods/src/patches/parts/mod_confirmation_settings.cc @@ -5,11 +5,16 @@ #include "settings/native_boolean_callback.h" #include #include +#include +#include #include +#include #include #include #include +#include #include +#include namespace { @@ -18,18 +23,32 @@ constexpr const char* CategoryKey = "game_settings_category_7"; constexpr std::size_t ViewLimit = 8; bool active = false; bool installing = false; -bool warned = false; std::thread::id uiThread; NativeCallback getter; NativeCallback setter; NativeCallback query; -void Warn() +void Warn(const char* operation) { - if (!warned) { - warned = true; - spdlog::warn("[ModSettings] Native confirmation UI unavailable; no mod control added"); + std::string message(operation); + if (auto exception = std::current_exception()) { + try { + std::rethrow_exception(exception); + } catch (const std::exception& error) { + message += ": "; + message += error.what(); + } catch (...) { + message += ": non-standard C++ exception"; + } + } + static std::mutex mutex; + static std::unordered_set reported; + { + std::lock_guard lock(mutex); + if (!reported.insert(message).second) + return; } + spdlog::warn("[ModSettings] {}", message); } struct Root { @@ -69,8 +88,16 @@ bool Instance(const MethodInfo* method, int count, int result) FieldInfo* Field(Il2CppClass* cls, const char* name) { auto* field = cls ? il2cpp_class_get_field_from_name(cls, name) : nullptr; - if (!field || !Reference(field->type) || (field->type->attrs & FIELD_ATTRIBUTE_STATIC)) - throw std::runtime_error("settings reference field"); + if (!field || !field->type || !Reference(field->type) || (field->type->attrs & FIELD_ATTRIBUTE_STATIC)) { + const std::string member = std::string(cls ? cls->namespaze : "") + "." + + (cls ? cls->name : "") + "." + name; + throw std::runtime_error(member + ": expected instance reference field; " + + (!field ? "field missing" : !field->type ? "type missing" + : "actual type=" + std::to_string(field->type->type) + + ", static=" + + std::to_string(bool(field->type->attrs & FIELD_ATTRIBUTE_STATIC)) + + ", byref=" + std::to_string(field->type->byref))); + } return field; } Il2CppObject* ReadField(Il2CppObject* object, FieldInfo* field) @@ -83,21 +110,22 @@ Il2CppObject* ReadField(Il2CppObject* object, FieldInfo* field) Il2CppObject* Invoke(const MethodInfo* method, Il2CppObject* object, void** args = nullptr) { if (!method || !object) - throw std::runtime_error("settings invocation"); + throw std::runtime_error(std::string("settings invocation: ") + + (method ? method->klass->name : "") + "." + + (method ? method->name : "?") + (object ? "" : ": missing receiver")); Il2CppObject* result = nullptr; if (!Il2CppRuntime::TryInvoke(method, object, args, &result)) - throw std::runtime_error("settings managed exception"); + throw std::runtime_error(std::string(method->klass->name) + "." + method->name + ": managed invocation failed"); return result; } // Bounded discovery helpers used only while opening a page or binding a row. Il2CppObject* Call(Il2CppObject* object, const char* name, int count = 0, void** args = nullptr) -{ return Invoke(object ? IL2CppClassHelper(object->klass).GetMethodInfo(name, count) : nullptr, object, args); } -bool Boolean(Il2CppObject* boxed) { - bool value = false; - if (!Il2CppRuntime::TryBoolean(boxed, value)) - throw std::runtime_error("settings boolean result"); - return value; + auto* method = object ? IL2CppClassHelper(object->klass).GetMethodInfo(name, count) : nullptr; + if (!method) + throw std::runtime_error(std::string(object ? object->klass->name : "") + "." + name + "(" + + std::to_string(count) + " arguments): method unavailable"); + return Invoke(method, object, args); } bool Equals(Il2CppObject* value, const char* ascii) { @@ -204,7 +232,7 @@ void Clear(View& view) try { Restore(view); } catch (...) { - Warn(); + Warn("confirmation view restoration unavailable"); } Free(view.widget); Free(view.context); @@ -231,7 +259,7 @@ bool Owned(Il2CppObject* context) bool ChildOf(Il2CppObject* transform, Il2CppObject* parent) { void* args[] = {parent}; - return Boolean(Call(transform, "IsChildOf", 1, args)); + return Il2CppChecked::Boolean(Call(transform, "IsChildOf", 1, args)); } View& Track(Il2CppObject* widget, Il2CppObject* context) { @@ -436,7 +464,7 @@ void Render(View& view, auto original, Il2CppObject* widget) if (!view.state.value()) { // Capture all native values first (the two components may share a node). for (std::size_t i = 0; i < view.indicators.size(); ++i) - view.activeBefore[i] = Boolean(Call(Target(view.indicators[i]), "get_activeSelf")); + view.activeBefore[i] = Il2CppChecked::Boolean(Call(Target(view.indicators[i]), "get_activeSelf")); view.hidden = true; for (auto handle : view.indicators) SetActive(Target(handle), false); @@ -448,8 +476,9 @@ void HideUnsupported(Il2CppObject* widget) Root object(Call(widget, "get_gameObject")); SetActive(object.get(), false); } catch (...) { + Warn("unsupported confirmation widget hiding unavailable"); } - Warn(); + Warn("confirmation widget unavailable"); } bool OnThread() { return active && std::this_thread::get_id() == uiThread; } @@ -462,7 +491,7 @@ void AddGeneralHook(auto original, Il2CppObject* director, Il2CppObject* context try { AddRow(director, context); } catch (...) { - Warn(); + Warn("confirmation row insertion unavailable"); } } void RefreshHook(auto original, Il2CppObject* widget) @@ -508,7 +537,7 @@ void RefreshHook(auto original, Il2CppObject* widget) HideUnsupported(widget); return; } - Warn(); + Warn("confirmation widget refresh unavailable"); } original(widget); } @@ -570,7 +599,7 @@ void ChangedHook(auto original, Il2CppObject* widget, bool desired) HideUnsupported(widget); return; } - Warn(); + Warn("confirmation value change unavailable"); } original(widget, desired); } @@ -581,7 +610,7 @@ void ReleaseHook(auto original, Il2CppObject* widget) if (auto* view = Find(widget)) Clear(*view); } catch (...) { - Warn(); + Warn("confirmation widget release unavailable"); } } original(widget); @@ -608,7 +637,7 @@ void SessionBoundary(auto original, Il2CppObject* owner) try { Invalidate(); } catch (...) { - Warn(); + Warn("confirmation session invalidation unavailable"); } } original(owner); @@ -629,45 +658,71 @@ void InstallModConfirmationSettings() try { auto& m = Meta(); const std::array hooks{m.addGeneral, m.refresh, m.changed, m.release, m.reload, m.session, m.load}; + const std::array names{"SettingsSectionDirector.AddGeneralSettings", "ToggleOptionWidget.SetWidgetData", + "ToggleOptionWidget.OnToggleValueChanged", "ToggleOptionWidget.OnAboutToReleaseContext", + "PersistentPrefsManager.RegisterEvents", "PersistentPrefsManager.GameSessionStartedEventHandler", + "PersistentPrefsManager.LoadPersistentPrefsFromCloud"}; for (std::size_t i = 0; i < hooks.size(); ++i) { if (!Instance(hooks[i], i == 0 || i == 2 ? 1 : 0, IL2CPP_TYPE_VOID)) - throw std::runtime_error("settings hook metadata"); + throw std::runtime_error(std::string(names[i]) + ": expected instance Void hook with " + + std::to_string(i == 0 || i == 2 ? 1 : 0) + " arguments"); for (std::size_t j = 0; j < i; ++j) if (hooks[i]->methodPointer == hooks[j]->methodPointer) - throw std::runtime_error("settings shared hook"); + throw std::runtime_error(std::string(names[i]) + " shares hook target with " + names[j]); } const auto* getSchema = m.director.GetMethodInfo("IsBorgCubeCuttingBeamConfirmationOn", 0); const auto* setSchema = m.director.GetMethodInfo("ToggleBorgCubeCuttingBeamConfirmation", 1); const auto* querySchema = m.director.GetMethodInfo("QueryShouldShowGenericPcSetting", 0); - if (!Instance(getSchema, 0, IL2CPP_TYPE_BOOLEAN) || !Instance(setSchema, 1, IL2CPP_TYPE_VOID) - || !Type(setSchema->parameters[0], IL2CPP_TYPE_BOOLEAN) || !querySchema + if (!Instance(getSchema, 0, IL2CPP_TYPE_BOOLEAN)) + throw std::runtime_error("SettingsSectionDirector.IsBorgCubeCuttingBeamConfirmationOn: expected instance Boolean()"); + if (!Instance(setSchema, 1, IL2CPP_TYPE_VOID) || !Type(setSchema->parameters[0], IL2CPP_TYPE_BOOLEAN)) + throw std::runtime_error("SettingsSectionDirector.ToggleBorgCubeCuttingBeamConfirmation: expected instance Void(Boolean)"); + if (!querySchema || !il2cpp_class_is_enum(il2cpp_class_from_type(querySchema->return_type)) || !Type(il2cpp_class_enum_basetype(il2cpp_class_from_type(querySchema->return_type)), IL2CPP_TYPE_I4) - || !Instance(querySchema, 0, IL2CPP_TYPE_VALUETYPE) || !Instance(m.addToggle, 4, IL2CPP_TYPE_VOID) + || !Instance(querySchema, 0, IL2CPP_TYPE_VALUETYPE)) + throw std::runtime_error("SettingsSectionDirector.QueryShouldShowGenericPcSetting: expected instance Int32 enum()"); + if (!Instance(m.addToggle, 4, IL2CPP_TYPE_VOID) || !Type(m.addToggle->parameters[1], IL2CPP_TYPE_STRING) || !Reference(m.addToggle->parameters[0]) - || !Reference(m.addToggle->parameters[2]) || !Reference(m.addToggle->parameters[3]) - || !Type(m.changed->parameters[0], IL2CPP_TYPE_BOOLEAN) || !Reference(m.addGeneral->parameters[0]) - || !m.getContext || !Reference(m.getContext->return_type) || !Instance(m.querySetter, 1, IL2CPP_TYPE_VOID) - || !Reference(m.querySetter->parameters[0]) || !getter.Initialize(getSchema, GetEnabled) - || !setter.Initialize(setSchema, SetEnabled) || !query.Initialize(querySchema, QueryState)) - throw std::runtime_error("settings callback schema"); + || !Reference(m.addToggle->parameters[2]) || !Reference(m.addToggle->parameters[3])) + throw std::runtime_error("SettingsContext.AddToggle: expected Void(reference, String, reference, reference)"); + if (!Type(m.changed->parameters[0], IL2CPP_TYPE_BOOLEAN)) + throw std::runtime_error("ToggleOptionWidget.OnToggleValueChanged: expected Boolean parameter"); + if (!Reference(m.addGeneral->parameters[0])) + throw std::runtime_error("SettingsSectionDirector.AddGeneralSettings: expected reference parameter"); + if (!m.getContext || !Reference(m.getContext->return_type)) + throw std::runtime_error("ToggleOptionWidget.get_Context: expected reference result"); + if (!Instance(m.querySetter, 1, IL2CPP_TYPE_VOID) || !Reference(m.querySetter->parameters[0])) + throw std::runtime_error("ToggleOptionContext.set_QueryOptionState: expected instance Void(reference)"); + if (!getter.Initialize(getSchema, GetEnabled)) + throw std::runtime_error("FC confirmation getter: incompatible native callback schema"); + if (!setter.Initialize(setSchema, SetEnabled)) + throw std::runtime_error("FC confirmation setter: incompatible native callback schema"); + if (!query.Initialize(querySchema, QueryState)) + throw std::runtime_error("FC confirmation query: incompatible native callback schema"); uiThread = std::this_thread::get_id(); if (!FleetCommanderConfirmationSetting().SetChangeObserver(RefreshViews)) throw std::runtime_error("settings observer ownership"); // A rejected target need not throw. Keep any installed hooks on their native // path until the complete adapter is ready; do not retry a partial install. - if (!SPUD_STATIC_DETOUR(m.refresh->methodPointer, RefreshHook) - || !SPUD_STATIC_DETOUR(m.changed->methodPointer, ChangedHook) - || !SPUD_STATIC_DETOUR(m.release->methodPointer, ReleaseHook) - || !SPUD_STATIC_DETOUR(m.reload->methodPointer, ReloadHook) - || !SPUD_STATIC_DETOUR(m.session->methodPointer, SessionHook) - || !SPUD_STATIC_DETOUR(m.load->methodPointer, LoadHook) - || !SPUD_STATIC_DETOUR(m.addGeneral->methodPointer, AddGeneralHook)) - throw std::runtime_error("settings hook installation"); + if (!SPUD_STATIC_DETOUR(m.refresh->methodPointer, RefreshHook)) + throw std::runtime_error("ToggleOptionWidget.SetWidgetData: hook installation failed"); + if (!SPUD_STATIC_DETOUR(m.changed->methodPointer, ChangedHook)) + throw std::runtime_error("ToggleOptionWidget.OnToggleValueChanged: hook installation failed"); + if (!SPUD_STATIC_DETOUR(m.release->methodPointer, ReleaseHook)) + throw std::runtime_error("ToggleOptionWidget.OnAboutToReleaseContext: hook installation failed"); + if (!SPUD_STATIC_DETOUR(m.reload->methodPointer, ReloadHook)) + throw std::runtime_error("PersistentPrefsManager.RegisterEvents: hook installation failed"); + if (!SPUD_STATIC_DETOUR(m.session->methodPointer, SessionHook)) + throw std::runtime_error("PersistentPrefsManager.GameSessionStartedEventHandler: hook installation failed"); + if (!SPUD_STATIC_DETOUR(m.load->methodPointer, LoadHook)) + throw std::runtime_error("PersistentPrefsManager.LoadPersistentPrefsFromCloud: hook installation failed"); + if (!SPUD_STATIC_DETOUR(m.addGeneral->methodPointer, AddGeneralHook)) + throw std::runtime_error("SettingsSectionDirector.AddGeneralSettings: hook installation failed"); active = true; spdlog::info("[ModSettings] Native FC confirmation adapter installed"); } catch (...) { - Warn(); + Warn("confirmation installation unavailable"); } } #else