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/runtime.h b/mods/src/il2cpp/runtime.h new file mode 100644 index 000000000..b3043673e --- /dev/null +++ b/mods/src/il2cpp/runtime.h @@ -0,0 +1,40 @@ +#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 +{ +// 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) + 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) + return false; + value = *data; + return true; +} +} // namespace Il2CppRuntime 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 new file mode 100644 index 000000000..b098a2bbc --- /dev/null +++ b/tests/il2cpp_runtime.cc @@ -0,0 +1,87 @@ +// 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 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 +#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(const Il2CppType*, il2cpp_class_get_type, (Il2CppClass*)) +{ return null_type ? nullptr : &type; } +END_API +API(void*, il2cpp_object_unbox, (Il2CppObject*)) +{ return null_unbox ? nullptr : &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() +{ + 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_BOOLEAN; + type.byref = true; + 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"; +} 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