From 2a0c0d58bc8bfd98c11506e82d6729ef6038a158 Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Wed, 10 Jun 2026 11:27:14 +0300 Subject: [PATCH 1/9] Fix Linux/clang detection in CMake Replace the CMake>=3.25-only LINUX variable with UNIX AND NOT APPLE, and the non-existent CLANG variable with a CMAKE_CXX_COMPILER_ID match so the clang libc++ flags are actually applied. Co-Authored-By: Claude Opus 4.8 (1M context) --- CMakeLists.txt | 4 +++- sample/CMakeLists.txt | 4 ++-- sample_shared/CMakeLists.txt | 6 ++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dd173c58..32bb790b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,7 +193,9 @@ elseif(APPLE) create_source_groups(MACOS_SOURCES) -elseif(LINUX) +elseif(UNIX AND NOT APPLE) + # Linux (the LINUX variable only exists in CMake >= 3.25; UNIX AND NOT APPLE + # works on all supported versions, so GUID_STDLIB is always defined here). set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGUID_STDLIB -std=c++17") if (CMAKE_CXX_COMPILER MATCHES "clang") diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index f8015670..1c7fef42 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -7,10 +7,10 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release") set(CMAKE_CXX_STANDARD 17) -if(LINUX) +if(UNIX AND NOT APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") - if(CLANG) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() diff --git a/sample_shared/CMakeLists.txt b/sample_shared/CMakeLists.txt index ed1e7821..c9cae162 100644 --- a/sample_shared/CMakeLists.txt +++ b/sample_shared/CMakeLists.txt @@ -6,10 +6,12 @@ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/Release") set(CMAKE_CXX_STANDARD 17) -if(LINUX) +if(UNIX AND NOT APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") - if(CLANG) + # Match the library's standard-library choice so the sample's ABI lines up + # with libGameAnalytics when built with clang. + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() endif() From 653c115970ed166849e74ae1cd3908eac9ca7d70 Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Wed, 10 Jun 2026 11:27:21 +0300 Subject: [PATCH 2/9] Use static-md vcpkg triplet for Windows shared builds Statically link curl/openssl/zlib into the DLL so the Unity plugin is a single self-contained GameAnalytics.dll. The -static-md variant keeps the dynamic CRT (/MD) to match shared builds. Co-Authored-By: Claude Opus 4.8 (1M context) --- setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup.py b/setup.py index bc97e123..aee990bf 100644 --- a/setup.py +++ b/setup.py @@ -101,6 +101,14 @@ def main(): triplet = f'{arch}-{platform}' + # On Windows, statically link curl/openssl/zlib into the DLL so the Unity + # plugin is a single self-contained GameAnalytics.dll (mirrors macOS, where + # vcpkg's default triplet already produces static deps). The "-static-md" + # variant keeps the dynamic CRT (/MD), matching CMAKE_MSVC_RUNTIME_LIBRARY + # for shared builds; plain "-static" would use /MT and conflict. + if args.platform.startswith('win') and args.shared: + triplet = f'{arch}-windows-static-md' + if args.platform == 'osx': osx_arch = arch if arch == 'arm64' else 'x86_64' # no official universal triplet for osx vcpkg cmake_command += f' -DVCPKG_HOST_TRIPLET={triplet}' From c91c88a130d7f25b3e1665514098cadca55b576a Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Wed, 10 Jun 2026 11:27:21 +0300 Subject: [PATCH 3/9] Self-heal incompatible cached SDK config Discard an unparseable cached config (e.g. legacy C# base64 format) instead of logging it as an error; a fresh config is fetched on init and rewritten as JSON. Co-Authored-By: Claude Opus 4.8 (1M context) --- source/gameanalytics/GAState.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/gameanalytics/GAState.cpp b/source/gameanalytics/GAState.cpp index f1e8d8de..ae1b6efe 100644 --- a/source/gameanalytics/GAState.cpp +++ b/source/gameanalytics/GAState.cpp @@ -637,7 +637,11 @@ namespace gameanalytics } catch (json::exception& e) { - logging::GALogger::e(e.what()); + // Cached config is unparseable (e.g. written by the legacy C# SDK + // in binary-base64 instead of JSON). Discard the stale row; a fresh + // config is fetched on init and rewritten as JSON, so this self-heals. + logging::GALogger::d("Discarding incompatible cached sdk config: %s", e.what()); + store::GAStore::setState("sdk_config_cached", ""); } } From c247cbc43b5dd0340979bcfd1f1e0f4d22690572 Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Wed, 10 Jun 2026 13:37:10 +0300 Subject: [PATCH 4/9] Update GALinux.cpp --- source/gameanalytics/Platform/GALinux.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/source/gameanalytics/Platform/GALinux.cpp b/source/gameanalytics/Platform/GALinux.cpp index 417afb80..ea3a7c5f 100644 --- a/source/gameanalytics/Platform/GALinux.cpp +++ b/source/gameanalytics/Platform/GALinux.cpp @@ -81,9 +81,19 @@ std::string gameanalytics::GAPlatformLinux::getOSVersion() std::string version; int const strSize = strlen(info.release); + int dotCount = 0; for (size_t i = 0; i < strSize; ++i) { - if (!isdigit(info.release[i]) && info.release[i] != '.') + if (info.release[i] == '.') + { + ++dotCount; + if (dotCount == 3) + { + version = std::string(info.release, info.release + i); + break; + } + } + else if (!isdigit(info.release[i])) { version = std::string(info.release, info.release + i); break; From dd2b075649d59a60a7eae50040bdba750f6b8199 Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Fri, 7 Aug 2026 16:32:46 +0300 Subject: [PATCH 5/9] Add remote configs listener to C API --- CHANGELOG.md | 6 +++ README.md | 31 +++++++++++++ sample_shared/Main.cpp | 9 ++++ source/gameanalytics/GameAnalyticsExtern.cpp | 46 ++++++++++++++++++++ source/gameanalytics/GameAnalyticsExtern.h | 8 ++++ 5 files changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2c91c3..1e1f17e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added + +- **Remote configs listener in the C API** — New `gameAnalytics_configureRemoteConfigsListener()` with `GARemoteConfigsListener` callback, making remote configs notifications usable from managed runtimes (e.g. Unity P/Invoke). Single listener; re-registering replaces it, `NULL` unregisters. + ## 5.2.0 ### Added diff --git a/README.md b/README.md index 26602df9..502b902e 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,37 @@ void myLogHandler(const char* message, GALoggerMessageType type) gameAnalytics_configureCustomLogHandler(myLogHandler); ``` +### Remote configs listener +To be notified whenever remote configs are populated: + +**C++ API:** +``` c++ +struct RemoteConfigs: public gameanalytics::IRemoteConfigsListener +{ + void onRemoteConfigsUpdated(std::string const& configs) override + { + // configs have been updated + } +}; + +auto listener = std::make_shared(); +gameanalytics::GameAnalytics::addRemoteConfigsListener(listener); +``` + +**C API (shared lib):** +```c +void myRemoteConfigsListener(const char* configs) +{ + // configs have been updated +} + +gameAnalytics_configureRemoteConfigsListener(myRemoteConfigsListener); +``` + +The C API supports a single listener; registering again replaces the previous one and passing `NULL` unregisters. Register it before `gameAnalytics_initialize` to not miss the first update. + +> **Note:** The listener is invoked on the SDK's internal thread and the `configs` string is only valid during the call — copy it if needed (do not pass it to `gameAnalytics_freeString`). + ### Custom HTTP client By default, the SDK uses cURL for HTTP requests. If you need to use a different HTTP library (e.g. on consoles or custom platforms), you can provide your own implementation by subclassing `GAHttpClient`: diff --git a/sample_shared/Main.cpp b/sample_shared/Main.cpp index 853cac95..281b3d62 100644 --- a/sample_shared/Main.cpp +++ b/sample_shared/Main.cpp @@ -13,6 +13,12 @@ void testCrash() *i = 10; } +void onRemoteConfigsUpdated(const char* configs) +{ + // called on the SDK's internal thread; configs is only valid for this call + std::cout << "remote configs = " << (configs ? configs : "") << '\n'; +} + int main(int argc, char** argv) { std::cout << "start\n"; @@ -44,6 +50,9 @@ int main(int argc, char** argv) gameAnalytics_setCustomDimension01("test"); + // must be registered before initialize, or the first payload can be missed + gameAnalytics_configureRemoteConfigsListener(onRemoteConfigsUpdated); + using namespace std::chrono_literals; gameAnalytics_initialize(GAME_KEY, SECRET_KEY); diff --git a/source/gameanalytics/GameAnalyticsExtern.cpp b/source/gameanalytics/GameAnalyticsExtern.cpp index a73bf901..106a2087 100644 --- a/source/gameanalytics/GameAnalyticsExtern.cpp +++ b/source/gameanalytics/GameAnalyticsExtern.cpp @@ -6,6 +6,7 @@ #include "GAUtilities.h" #include #include +#include gameanalytics::StringVector makeStringVector(const char** arr, int size) { @@ -315,6 +316,51 @@ GA_API const char* gameAnalytics_getRemoteConfigsValueAsJson(const char* key) return gameAnalytics_allocString(returnValue); } +namespace +{ + // Adapts the C++ listener interface to a C function pointer + class ExternRemoteConfigsListener : public gameanalytics::IRemoteConfigsListener + { + public: + + explicit ExternRemoteConfigsListener(GARemoteConfigsListener listener): + _listener(listener) + { + } + + void onRemoteConfigsUpdated(std::string const& remoteConfigs) override + { + if (_listener) + { + _listener(remoteConfigs.c_str()); + } + } + + private: + + GARemoteConfigsListener _listener; + }; + + std::shared_ptr g_externRemoteConfigsListener; +} + +GA_API void gameAnalytics_configureRemoteConfigsListener(GARemoteConfigsListener listener) +{ + if (g_externRemoteConfigsListener) + { + gameanalytics::GameAnalytics::removeRemoteConfigsListener(g_externRemoteConfigsListener); + g_externRemoteConfigsListener.reset(); + } + + if (!listener) + { + return; + } + + g_externRemoteConfigsListener = std::make_shared(listener); + gameanalytics::GameAnalytics::addRemoteConfigsListener(g_externRemoteConfigsListener); +} + GA_API const char* gameAnalytics_getABTestingId() { std::string returnValue = gameanalytics::GameAnalytics::getABTestingId(); diff --git a/source/gameanalytics/GameAnalyticsExtern.h b/source/gameanalytics/GameAnalyticsExtern.h index c43f528f..02de4876 100644 --- a/source/gameanalytics/GameAnalyticsExtern.h +++ b/source/gameanalytics/GameAnalyticsExtern.h @@ -67,6 +67,10 @@ enum GALoggerMessageType typedef float(*GAFpsTracker)(void); typedef void(*GALogHandler)(const char* message, GALoggerMessageType messageType); +// called when remote configs are populated (on the SDK's internal thread) +// the string is only valid during the call - copy it if needed, do not free it +typedef void(*GARemoteConfigsListener)(const char* remoteConfigs); + GA_API void gameAnalytics_freeString(const char* ptr); GA_API void gameAnalytics_configureAvailableCustomDimensions01(const char **customDimensions, int size); @@ -138,6 +142,10 @@ GA_API const char* gameAnalytics_getExternalUserId(); GA_API GAStatus gameAnalytics_isRemoteConfigsReady(); GA_API const char* gameAnalytics_getRemoteConfigsContentAsString(); +// single listener; registering again replaces it, NULL unregisters +// register before initialize to not miss the first update +GA_API void gameAnalytics_configureRemoteConfigsListener(GARemoteConfigsListener listener); + GA_API const char* gameAnalytics_getABTestingId(); GA_API const char* gameAnalytics_getABTestingVariantId(); From 1f5e5a0455949ac1f638dd30dafd6828ef583626 Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Mon, 10 Aug 2026 10:33:48 +0300 Subject: [PATCH 6/9] fix comments --- sample_shared/Main.cpp | 3 +-- source/gameanalytics/GameAnalyticsExtern.cpp | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/sample_shared/Main.cpp b/sample_shared/Main.cpp index 281b3d62..6d2191ce 100644 --- a/sample_shared/Main.cpp +++ b/sample_shared/Main.cpp @@ -15,7 +15,6 @@ void testCrash() void onRemoteConfigsUpdated(const char* configs) { - // called on the SDK's internal thread; configs is only valid for this call std::cout << "remote configs = " << (configs ? configs : "") << '\n'; } @@ -50,7 +49,7 @@ int main(int argc, char** argv) gameAnalytics_setCustomDimension01("test"); - // must be registered before initialize, or the first payload can be missed + // register before initialize or the first update is missed gameAnalytics_configureRemoteConfigsListener(onRemoteConfigsUpdated); using namespace std::chrono_literals; diff --git a/source/gameanalytics/GameAnalyticsExtern.cpp b/source/gameanalytics/GameAnalyticsExtern.cpp index 106a2087..17b3b4bb 100644 --- a/source/gameanalytics/GameAnalyticsExtern.cpp +++ b/source/gameanalytics/GameAnalyticsExtern.cpp @@ -318,7 +318,6 @@ GA_API const char* gameAnalytics_getRemoteConfigsValueAsJson(const char* key) namespace { - // Adapts the C++ listener interface to a C function pointer class ExternRemoteConfigsListener : public gameanalytics::IRemoteConfigsListener { public: From 99c6d83bcc8c30ec13c4c09de068e4accf49c9aa Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Mon, 10 Aug 2026 12:55:48 +0300 Subject: [PATCH 7/9] fix exported symbols --- CMakeIncludes/exported_symbols_apple.txt | 3 +++ CMakeLists.txt | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 CMakeIncludes/exported_symbols_apple.txt diff --git a/CMakeIncludes/exported_symbols_apple.txt b/CMakeIncludes/exported_symbols_apple.txt new file mode 100644 index 00000000..5e83c82f --- /dev/null +++ b/CMakeIncludes/exported_symbols_apple.txt @@ -0,0 +1,3 @@ +# Symbols exported from the GameAnalytics shared library on Apple platforms. +# Used via -Wl,-exported_symbols_list. Glob patterns are supported by ld64. +_gameAnalytics_* diff --git a/CMakeLists.txt b/CMakeLists.txt index dd173c58..ed462369 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,6 +213,24 @@ endif() add_library(GameAnalytics ${LIB_TYPE} ${CPP_SOURCES}) target_link_libraries(GameAnalytics PRIVATE ${LIBS} PUBLIC ${PUBLIC_LIBS}) + +# Hide symbols by default (GCC/Clang -fvisibility=hidden); GA_API in GameAnalyticsExtern.h +# still marks the public C API as exported. No-op on MSVC (dllexport/dllimport controls that). +set_target_properties(GameAnalytics PROPERTIES + C_VISIBILITY_PRESET hidden + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) + +if(${GA_SHARED_LIB}) + if(APPLE) + target_link_options(GameAnalytics PRIVATE + "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/CMakeIncludes/exported_symbols_apple.txt") + elseif(UNIX) + target_link_options(GameAnalytics PRIVATE "-Wl,--exclude-libs,ALL") + endif() +endif() + message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") message(STATUS "CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}") message(STATUS "CMAKE_SHARED_LINKER_FLAGS: ${CMAKE_SHARED_LINKER_FLAGS}") From d17034008fd1d20b65f16fff443ab4bb7010023b Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Mon, 10 Aug 2026 15:38:40 +0300 Subject: [PATCH 8/9] fix linux --- CMakeLists.txt | 5 ++--- sample_shared/CMakeLists.txt | 2 -- setup.py | 6 +----- source/gameanalytics/GAState.cpp | 4 +--- source/gameanalytics/Platform/GALinux.cpp | 14 ++++++++------ 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 32bb790b..e4603c81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -194,11 +194,10 @@ elseif(APPLE) create_source_groups(MACOS_SOURCES) elseif(UNIX AND NOT APPLE) - # Linux (the LINUX variable only exists in CMake >= 3.25; UNIX AND NOT APPLE - # works on all supported versions, so GUID_STDLIB is always defined here). + # Linux set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DGUID_STDLIB -std=c++17") - if (CMAKE_CXX_COMPILER MATCHES "clang") + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") message(STATUS "Detected Clang compiler: ${CMAKE_CXX_COMPILER}") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() diff --git a/sample_shared/CMakeLists.txt b/sample_shared/CMakeLists.txt index c9cae162..b0107ac4 100644 --- a/sample_shared/CMakeLists.txt +++ b/sample_shared/CMakeLists.txt @@ -9,8 +9,6 @@ set(CMAKE_CXX_STANDARD 17) if(UNIX AND NOT APPLE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") - # Match the library's standard-library choice so the sample's ABI lines up - # with libGameAnalytics when built with clang. if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -stdlib=libc++") endif() diff --git a/setup.py b/setup.py index aee990bf..df76841f 100644 --- a/setup.py +++ b/setup.py @@ -101,11 +101,7 @@ def main(): triplet = f'{arch}-{platform}' - # On Windows, statically link curl/openssl/zlib into the DLL so the Unity - # plugin is a single self-contained GameAnalytics.dll (mirrors macOS, where - # vcpkg's default triplet already produces static deps). The "-static-md" - # variant keeps the dynamic CRT (/MD), matching CMAKE_MSVC_RUNTIME_LIBRARY - # for shared builds; plain "-static" would use /MT and conflict. + # static deps, dynamic CRT (/MD) if args.platform.startswith('win') and args.shared: triplet = f'{arch}-windows-static-md' diff --git a/source/gameanalytics/GAState.cpp b/source/gameanalytics/GAState.cpp index ae1b6efe..1e29be83 100644 --- a/source/gameanalytics/GAState.cpp +++ b/source/gameanalytics/GAState.cpp @@ -637,9 +637,7 @@ namespace gameanalytics } catch (json::exception& e) { - // Cached config is unparseable (e.g. written by the legacy C# SDK - // in binary-base64 instead of JSON). Discard the stale row; a fresh - // config is fetched on init and rewritten as JSON, so this self-heals. + // discard unparseable cached config logging::GALogger::d("Discarding incompatible cached sdk config: %s", e.what()); store::GAStore::setState("sdk_config_cached", ""); } diff --git a/source/gameanalytics/Platform/GALinux.cpp b/source/gameanalytics/Platform/GALinux.cpp index ea3a7c5f..4862b49d 100644 --- a/source/gameanalytics/Platform/GALinux.cpp +++ b/source/gameanalytics/Platform/GALinux.cpp @@ -78,24 +78,26 @@ std::string gameanalytics::GAPlatformLinux::getOSVersion() struct utsname info; uname(&info); - std::string version; - int const strSize = strlen(info.release); + std::string version = info.release; + size_t const strSize = version.size(); int dotCount = 0; for (size_t i = 0; i < strSize; ++i) { - if (info.release[i] == '.') + char const c = version[i]; + + if (c == '.') { ++dotCount; if (dotCount == 3) { - version = std::string(info.release, info.release + i); + version.resize(i); break; } } - else if (!isdigit(info.release[i])) + else if (!isdigit(static_cast(c))) { - version = std::string(info.release, info.release + i); + version.resize(i); break; } } From 124b4b50f6440edf8dbc23798c36cbd4fdfac44b Mon Sep 17 00:00:00 2001 From: Andrei Dabija Date: Mon, 10 Aug 2026 15:44:51 +0300 Subject: [PATCH 9/9] fix cpp static to use /MT --- setup.py | 6 +++--- source/gameanalytics/GAState.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index df76841f..b5402a6f 100644 --- a/setup.py +++ b/setup.py @@ -101,9 +101,9 @@ def main(): triplet = f'{arch}-{platform}' - # static deps, dynamic CRT (/MD) - if args.platform.startswith('win') and args.shared: - triplet = f'{arch}-windows-static-md' + # match CMAKE_MSVC_RUNTIME_LIBRARY: /MD shared, /MT static + if args.platform.startswith('win'): + triplet = f'{arch}-windows-static-md' if args.shared else f'{arch}-windows-static' if args.platform == 'osx': osx_arch = arch if arch == 'arm64' else 'x86_64' # no official universal triplet for osx vcpkg diff --git a/source/gameanalytics/GAState.cpp b/source/gameanalytics/GAState.cpp index 1e29be83..ae6f694a 100644 --- a/source/gameanalytics/GAState.cpp +++ b/source/gameanalytics/GAState.cpp @@ -638,7 +638,7 @@ namespace gameanalytics catch (json::exception& e) { // discard unparseable cached config - logging::GALogger::d("Discarding incompatible cached sdk config: %s", e.what()); + logging::GALogger::w("Discarding incompatible cached sdk config: %s", e.what()); store::GAStore::setState("sdk_config_cached", ""); } }