diff --git a/CMakeLists.txt b/CMakeLists.txt index 52a5c52..802dd6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -253,6 +253,7 @@ if(NOT GA_SHARED_LIB) ######################################## file(GLOB_RECURSE TEST_SRC_FILES "${PROJECT_SOURCE_DIR}/test/*.cpp") + list(FILTER TEST_SRC_FILES EXCLUDE REGEX "/test/allocator_boundary/") ######################################## # Unit Tests @@ -271,6 +272,18 @@ if(NOT GA_SHARED_LIB) ######################################## add_test(NAME ${UT_PROJECT_NAME} COMMAND GameAnalyticsUnitTests) + + if(APPLE) + # Mimics monolithic Unreal: private operator new/delete. + set(AB_TEST_NAME "${PROJECT_NAME}AllocatorBoundaryTest") + add_executable(${AB_TEST_NAME} "${PROJECT_SOURCE_DIR}/test/allocator_boundary/main.cpp") + target_link_libraries(${AB_TEST_NAME} ${PROJECT_NAME}) + file(WRITE "${CMAKE_BINARY_DIR}/no_exports.txt" "") + target_link_options(${AB_TEST_NAME} PRIVATE "LINKER:-exported_symbols_list,${CMAKE_BINARY_DIR}/no_exports.txt") + add_test(NAME ${AB_TEST_NAME} COMMAND ${AB_TEST_NAME}) + add_test(NAME ${PROJECT_NAME}NoLibcxxStringImports + COMMAND sh -c "! nm -u $ | grep -E '^_+ZNSt3__1(12basic_string|15basic_stringbuf|1[89]basic_[io]?stringstream|plI)'") + endif() else() message(STATUS "Skipping unit tests (not available for shared library builds)") endif() diff --git a/source/gameanalytics/GAHTTPApi.cpp b/source/gameanalytics/GAHTTPApi.cpp index 244ce00..5be09c1 100644 --- a/source/gameanalytics/GAHTTPApi.cpp +++ b/source/gameanalytics/GAHTTPApi.cpp @@ -323,59 +323,54 @@ namespace gameanalytics ErrorType errorType = std::make_tuple(category, area); - bool useGzip = this->useGzip; - - auto task = std::async(std::launch::async, [=]() -> void + int64_t now = utilities::GAUtilities::timeIntervalSince1970(); + if(timestampMap.count(errorType) == 0) { - int64_t now = utilities::GAUtilities::timeIntervalSince1970(); - if(timestampMap.count(errorType) == 0) - { - timestampMap[errorType] = now; - } - if(countMap.count(errorType) == 0) - { - countMap[errorType] = 0; - } - - constexpr int64_t FREQUENCY = 3600; // 1h + timestampMap[errorType] = now; + } + if(countMap.count(errorType) == 0) + { + countMap[errorType] = 0; + } + + constexpr int64_t FREQUENCY = 3600; // 1h - int64_t diff = now - timestampMap[errorType]; - if(diff >= FREQUENCY) - { - countMap[errorType] = 0; - timestampMap[errorType] = now; - } + int64_t diff = now - timestampMap[errorType]; + if(diff >= FREQUENCY) + { + countMap[errorType] = 0; + timestampMap[errorType] = now; + } - if(countMap[errorType] >= MaxCount) - { - return; - } + if(countMap[errorType] >= MaxCount) + { + return; + } - std::vector payloadData = getInstance().createPayloadData(payloadJSONString, useGzip); + std::vector payloadData = getInstance().createPayloadData(payloadJSONString, useGzip); - std::string auth = createAuth(payloadData); - GAHttpClient::Response response = impl->sendRequest(url, auth, payloadData, useGzip, nullptr); + std::string auth = createAuth(payloadData); + GAHttpClient::Response response = impl->sendRequest(url, auth, payloadData, useGzip, nullptr); - if(response.code < 0) - { - logging::GALogger::e("Request failed: %s", url.c_str()); - return; - } + if(response.code < 0) + { + logging::GALogger::e("Request failed: %s", url.c_str()); + return; + } - std::string_view content = response.toString(); + std::string_view content = response.toString(); - // process the response - logging::GALogger::d("sdk error content : %.*s", (int)content.size(), content.data()); + // process the response + logging::GALogger::d("sdk error content : %.*s", (int)content.size(), content.data()); - // if not 200 result - if (response.code != HTTP_RESPONSE_OK && response.code != HTTP_RESPONSE_NO_CONTENT) - { - logging::GALogger::d("sdk error failed. response code not 200 or 204. status code: %ld", response.code); - return; - } + // if not 200 result + if (response.code != HTTP_RESPONSE_OK && response.code != HTTP_RESPONSE_NO_CONTENT) + { + logging::GALogger::d("sdk error failed. response code not 200 or 204. status code: %ld", response.code); + return; + } - countMap[errorType] = countMap[errorType] + 1; - }); + countMap[errorType] = countMap[errorType] + 1; } std::vector GAHTTPApi::createPayloadData(std::string const& payload, bool gzip) diff --git a/source/gameanalytics/GAThread.h b/source/gameanalytics/GAThread.h new file mode 100644 index 0000000..60dd5f1 --- /dev/null +++ b/source/gameanalytics/GAThread.h @@ -0,0 +1,87 @@ +// +// GA-SDK-CPP +// Copyright 2018 GameAnalytics C++ SDK. All rights reserved. +// + +#pragma once + +#if !defined(__APPLE__) + +#include + +namespace gameanalytics::threading +{ + using GAThread = std::thread; +} + +#else + +#include +#include + +namespace gameanalytics::threading +{ + // std::thread frees its TLS block inside libc++.dylib. + class GAThread + { + public: + GAThread() = default; + + explicit GAThread(std::function fn) + { + auto* heapFn = new std::function(std::move(fn)); + _joinable = pthread_create(&_handle, nullptr, &GAThread::entry, heapFn) == 0; + if (!_joinable) + { + delete heapFn; + } + } + + ~GAThread() + { + join(); + } + + GAThread(GAThread&& other) noexcept + { + *this = std::move(other); + } + + GAThread& operator=(GAThread&& other) noexcept + { + join(); + _handle = other._handle; + _joinable = other._joinable; + other._joinable = false; + return *this; + } + + GAThread(GAThread const&) = delete; + GAThread& operator=(GAThread const&) = delete; + + bool joinable() const { return _joinable; } + + void join() + { + if (_joinable) + { + _joinable = false; + pthread_join(_handle, nullptr); + } + } + + private: + static void* entry(void* arg) + { + auto* fn = static_cast*>(arg); + (*fn)(); + delete fn; + return nullptr; + } + + pthread_t _handle = {}; + bool _joinable = false; + }; +} + +#endif diff --git a/source/gameanalytics/GAThreading.cpp b/source/gameanalytics/GAThreading.cpp index 4fd6253..f18003f 100644 --- a/source/gameanalytics/GAThreading.cpp +++ b/source/gameanalytics/GAThreading.cpp @@ -32,7 +32,7 @@ namespace gameanalytics GAThreading::GAThreading() { - _thread = std::thread( + _thread = GAThread( [this]() { work(); diff --git a/source/gameanalytics/GAThreading.h b/source/gameanalytics/GAThreading.h index 0b222bc..f6a270a 100644 --- a/source/gameanalytics/GAThreading.h +++ b/source/gameanalytics/GAThreading.h @@ -17,6 +17,7 @@ #include #include "GACommon.h" +#include "GAThread.h" namespace gameanalytics { @@ -71,7 +72,7 @@ namespace gameanalytics std::vector _tasks; std::queue _blocks; - std::thread _thread; + GAThread _thread; std::mutex _blockMutex; std::mutex _taskMutex; std::atomic _endThread = false; diff --git a/source/gameanalytics/GAValidator.cpp b/source/gameanalytics/GAValidator.cpp index f831836..608a30d 100644 --- a/source/gameanalytics/GAValidator.cpp +++ b/source/gameanalytics/GAValidator.cpp @@ -181,7 +181,10 @@ namespace gameanalytics out.action = http::EGASdkErrorAction::InvalidAmount; out.parameter = http::EGASdkErrorParameter::Amount; - out.reason = std::to_string(amount); + // std::to_string(double) allocates in libc++.dylib. + char amountStr[64] = {}; + std::snprintf(amountStr, sizeof(amountStr), "%f", amount); + out.reason = amountStr; return; } diff --git a/source/gameanalytics/Platform/GALibcxxInstantiations.cpp b/source/gameanalytics/Platform/GALibcxxInstantiations.cpp new file mode 100644 index 0000000..e3453c7 --- /dev/null +++ b/source/gameanalytics/Platform/GALibcxxInstantiations.cpp @@ -0,0 +1,15 @@ +// Keeps libc++ string allocations inside this binary. +#include +#include + +#if defined(__APPLE__) && defined(_LIBCPP_VERSION) + +template class std::basic_string; +template std::string std::operator+, std::allocator>(const char*, const std::string&); + +template class std::basic_stringbuf; +template class std::basic_stringstream; +template class std::basic_ostringstream; +template class std::basic_istringstream; + +#endif diff --git a/test/allocator_boundary/main.cpp b/test/allocator_boundary/main.cpp new file mode 100644 index 0000000..49c97c1 --- /dev/null +++ b/test/allocator_boundary/main.cpp @@ -0,0 +1,55 @@ +// Simulates a host replacing operator new/delete without exporting them. +#include +#include +#include + +#include "GameAnalytics/GameAnalytics.h" + +namespace +{ + constexpr unsigned long long kMagic = 0x4745554E47494E45ULL; + constexpr size_t kHeader = 16; +} + +void* operator new(size_t n) +{ + auto* p = static_cast(std::malloc(n + kHeader)); + if (!p) throw std::bad_alloc(); + p[0] = kMagic; + return p + 2; +} +void* operator new[](size_t n) { return operator new(n); } + +void operator delete(void* p) noexcept +{ + if (!p) return; + auto* q = static_cast(p) - 2; + if (q[0] != kMagic) + { + std::fprintf(stderr, "FATAL: freeing pointer %p allocated outside this binary\n", p); + std::abort(); + } + q[0] = 0; + std::free(q); +} +void operator delete[](void* p) noexcept { operator delete(p); } +void operator delete(void* p, size_t) noexcept { operator delete(p); } +void operator delete[](void* p, size_t) noexcept { operator delete(p); } + +int main() +{ + using namespace gameanalytics; + + GameAnalytics::setEnabledInfoLog(true); + GameAnalytics::setEnabledEventSubmission(false); + GameAnalytics::configureBuild("allocator-boundary-test 1.0"); + GameAnalytics::initialize("00000000000000000000000000000000", "0000000000000000000000000000000000000000"); + + GameAnalytics::addDesignEvent("allocator:boundary:check"); + GameAnalytics::addResourceEvent(EGAResourceFlowType::Sink, "gems", -1e20f, "boost", "speed"); + + GameAnalytics::onQuit(); + + std::printf("ALLOCATOR BOUNDARY TEST OK\n"); + return 0; +}