Skip to content

Commit

Permalink
Fix il2cpp thread util to be more or less equivalent to std::thread, …
Browse files Browse the repository at this point in the history
…and add tests
  • Loading branch information
RedBrumbler committed Dec 21, 2023
1 parent 351982b commit 7f8c281
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 60 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function(setup_target target add_test)
TEST_LIST
TEST_STRING
TEST_HOOK
TEST_THREAD
)
endif()

Expand Down
102 changes: 43 additions & 59 deletions shared/utils/il2cpp-utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "il2cpp-utils-properties.hpp"
#include "il2cpp-utils-fields.hpp"
#include <string>
#include <thread>
#include <string_view>
#include <sstream>
#include <optional>
Expand Down Expand Up @@ -699,66 +700,49 @@ namespace il2cpp_utils {
return out;
}

/// @brief method executed by the thread created in il2cpp_aware_thread
/// @param pred the predicate to use in the thread
/// @param args the args used
template<class Predicate, typename... TArgs>
void il2cpp_aware_thread_method(Predicate pred, TArgs&&... args) {
std::stringstream loggerContext; loggerContext << "Thread " << std::this_thread::get_id();
auto logger = getLogger().WithContext(loggerContext.str()); // logger is per thread id, can't be static

logger.info("Attaching thread");
auto domain = il2cpp_functions::domain_get();
auto thread = il2cpp_functions::thread_attach(domain);

logger.info("Invoking predicate");
pred(args...);

logger.info("Detaching thread");
il2cpp_functions::thread_detach(thread);
}

/// @brief method executed by the thread created in il2cpp_aware_thread
/// @param pred the predicate to use in the thread
/// @param args the args used
template<typename T, typename U, typename... TArgs>
requires(std::is_convertible_v<T, U>)
void il2cpp_aware_thread_method(T& instance, void (U::*method)(TArgs...), TArgs&&... args) {
std::stringstream loggerContext; loggerContext << "Thread " << std::this_thread::get_id();
auto logger = getLogger().WithContext(loggerContext.str()); // logger is per thread id, can't be static

logger.info("Attaching thread");
auto domain = il2cpp_functions::domain_get();
auto thread = il2cpp_functions::thread_attach(domain);

logger.info("Invoking predicate");
instance->*method(args...);

logger.info("Detaching thread");
il2cpp_functions::thread_detach(thread);
}

/// @brief creates a thread that automatically will register with il2cpp and deregister once it exits, ensure your args live longer than the thread if they're by reference!
/// @param pred the predicate to use for the thread
/// @param args the arguments to pass to the thread (& predicate)
/// @return created thread, which is the same as you creating a default one
template<class Predicate, typename... TArgs>
inline std::thread il2cpp_aware_thread(Predicate pred, TArgs&&... args) {
il2cpp_functions::Init();
return std::thread(&il2cpp_aware_thread_method<Predicate, TArgs...>, pred, std::forward(args)...);
}
struct il2cpp_aware_thread : public std::thread {
public:
/// @brief method executed by the thread created in il2cpp_aware_thread
/// @param pred the predicate to use in the thread
/// @param args the args used
template<typename Predicate, typename... TArgs>
static void internal_thread(Predicate&& pred, TArgs&&... args) {
il2cpp_functions::Init();
std::stringstream loggerContext; loggerContext << "Thread " << std::this_thread::get_id();
auto logger = getLogger().WithContext(loggerContext.str()); // logger is per thread id, can't be static

logger.info("Attaching thread");
auto domain = il2cpp_functions::domain_get();
auto thread = il2cpp_functions::thread_attach(domain);

logger.info("Invoking predicate");
if constexpr (sizeof...(TArgs) > 0) {
std::invoke(std::forward<Predicate>(pred), std::forward<TArgs>(args)...);
} else {
std::invoke(std::forward<Predicate>(pred));
}

logger.info("Detaching thread");
il2cpp_functions::thread_detach(thread);
}

/// @brief creates a thread that automatically will register with il2cpp and deregister once it exits, ensure your args live longer than the thread if they're by reference!
/// @param instance the instance on which the method will be called
/// @param method the member method to call
/// @param args the arguments to pass to the method
/// @return created thread, which is the same as you creating a default one
template<class T, typename U, typename... TArgs>
requires(std::is_convertible_v<T, U>)
inline std::thread il2cpp_aware_thread(T& instance, void (U::*method)(TArgs...), TArgs&&... args) {
il2cpp_functions::Init();
return std::thread(&il2cpp_aware_thread_method<T, U, TArgs...>, instance, method, std::forward(args)...);
}
/// @brief creates a thread that automatically will register with il2cpp and deregister once it exits, ensure your args live longer than the thread if they're by reference!
/// @param pred the predicate to use for the thread
/// @param args the arguments to pass to the thread (& predicate)
/// @return created thread, which is the same as you creating a default one
template<typename Predicate, typename... TArgs>
explicit il2cpp_aware_thread(Predicate&& pred, TArgs&&... args) :
std::thread(
&internal_thread<Predicate, std::decay_t<TArgs>...>,
std::forward<Predicate>(pred),
std::forward<TArgs>(args)...
)
{}

~il2cpp_aware_thread() {
if (joinable()) join();
}
};
}

#pragma pack(pop)
Expand Down
2 changes: 1 addition & 1 deletion src/tests/test-check.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#ifdef NO_TEST
#if defined(TEST_CALLBACKS) || defined(TEST_SAFEPTR) || defined(TEST_BYREF) || defined(TEST_ARRAY) || defined(TEST_LIST) || defined(TEST_STRING) || defined(TEST_HOOK)
#if defined(TEST_CALLBACKS) || defined(TEST_SAFEPTR) || defined(TEST_BYREF) || defined(TEST_ARRAY) || defined(TEST_LIST) || defined(TEST_STRING) || defined(TEST_HOOK) || defined(TEST_THREAD)
#error "tests are being built into the release for bs hook!"
#endif
#endif
109 changes: 109 additions & 0 deletions src/tests/thread-tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#ifdef TEST_THREAD
#include "utils/il2cpp-utils.hpp"

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
#pragma clang diagnostic ignored "-Wunused-parameter"

static void thread_method() {}
static void thread_method_value(int v) {}
static void thread_method_lvalue(int& v) {}
static void thread_method_rvalue(int&& v) {}

struct ThreadTest {
void method() {}

void method_value(int v) {}
void method_lvalue(int& v) {}
void method_rvalue(int&& v) {}

// can we do all the same things from an instance of a struct
void test_thread();

void some_call() {};
};

// test both il2cpp aware thread and std::thread for equivalence
#define IL2CPP_THREAD_TEST(...) \
il2cpp_utils::il2cpp_aware_thread(__VA_ARGS__).join(); \
std::thread(__VA_ARGS__).join()

void test_thread() {
// can we make a 0 arg lambda thread?
IL2CPP_THREAD_TEST([](){});

IL2CPP_THREAD_TEST(&thread_method);

int v = 0;

// can we capture?
IL2CPP_THREAD_TEST([=](){ int b = v + 1; });
IL2CPP_THREAD_TEST([&](){ v += 1;});
IL2CPP_THREAD_TEST([v](){ int b = v + 1; });
IL2CPP_THREAD_TEST([value = v](){ int b = value + 1; });
IL2CPP_THREAD_TEST([value = &v](){ *value += 1; });
IL2CPP_THREAD_TEST([&v](){ v += 1; });

// can we make a lambda that accepts an integer from lvalue & rvalue?
IL2CPP_THREAD_TEST([](int v){}, v);
IL2CPP_THREAD_TEST([](int v){}, 10);

// pass rvalue into lvalue
// IL2CPP_THREAD_TEST([]( int& v){}, 10); // should not work
// pass rvalue into rvalue
IL2CPP_THREAD_TEST([](int&& v){}, 10); // should work
// pass lvalue into lvalue
// IL2CPP_THREAD_TEST([]( int& v){}, v); // should work
// pass lvalue into rvalue
IL2CPP_THREAD_TEST([](int&& v){}, v); // should work

// the same for a method?
IL2CPP_THREAD_TEST(&thread_method_value, v); // should work
IL2CPP_THREAD_TEST(&thread_method_value, 10); // should work

// can we pass the value as appropriate?
// lvalue into lvalue
// IL2CPP_THREAD_TEST(&thread_method_lvalue, v); // should not work
// lvalue into rvalue
IL2CPP_THREAD_TEST(&thread_method_rvalue, v); // should work
// rvalue into lvalue
// IL2CPP_THREAD_TEST(&thread_method_lvalue, 10); // should not work
// rvalue into rvalue
IL2CPP_THREAD_TEST(&thread_method_rvalue, 10); // should work

ThreadTest tt;
tt.test_thread();
}

void ThreadTest::test_thread() {
// can we make a 0 arg lambda thread?
IL2CPP_THREAD_TEST([](){});
IL2CPP_THREAD_TEST(&ThreadTest::method, this);

int v = 0;

// can we capture?
IL2CPP_THREAD_TEST([this](){ some_call(); });
IL2CPP_THREAD_TEST([=](){ int b = v + 1; });
IL2CPP_THREAD_TEST([&](){ some_call(); });
IL2CPP_THREAD_TEST([v](){ int b = v + 1; });
IL2CPP_THREAD_TEST([value = v](){ int b = value + 1; });
IL2CPP_THREAD_TEST([value = &v](){ *value += 1; });
IL2CPP_THREAD_TEST([&v](){ v += 1; });

// can we make a lambda that accepts an integer from lvalue & rvalue?
IL2CPP_THREAD_TEST([](ThreadTest* self, int v){}, this, v);
IL2CPP_THREAD_TEST([](ThreadTest* self, int v){}, this, 10);

// the same for a method?
IL2CPP_THREAD_TEST(&ThreadTest::method_value, this, v);
IL2CPP_THREAD_TEST(&ThreadTest::method_value, this, 10);

// can we pass the value as appropriate?
// IL2CPP_THREAD_TEST(&ThreadTest::method_lvalue, this, v); // should not work
IL2CPP_THREAD_TEST(&ThreadTest::method_rvalue, this, 10);
}

#pragma clang diagnostic pop

#endif

0 comments on commit 7f8c281

Please sign in to comment.