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/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..2de1e94d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -193,10 +193,11 @@ elseif(APPLE) create_source_groups(MACOS_SOURCES) -elseif(LINUX) +elseif(UNIX AND NOT APPLE) + # 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() @@ -213,6 +214,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}") 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/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 059856c2..b0107ac4 100644 --- a/sample_shared/CMakeLists.txt +++ b/sample_shared/CMakeLists.txt @@ -6,7 +6,7 @@ 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(CMAKE_CXX_COMPILER_ID MATCHES "Clang") diff --git a/sample_shared/Main.cpp b/sample_shared/Main.cpp index 853cac95..6d2191ce 100644 --- a/sample_shared/Main.cpp +++ b/sample_shared/Main.cpp @@ -13,6 +13,11 @@ void testCrash() *i = 10; } +void onRemoteConfigsUpdated(const char* configs) +{ + std::cout << "remote configs = " << (configs ? configs : "") << '\n'; +} + int main(int argc, char** argv) { std::cout << "start\n"; @@ -44,6 +49,9 @@ int main(int argc, char** argv) gameAnalytics_setCustomDimension01("test"); + // register before initialize or the first update is missed + gameAnalytics_configureRemoteConfigsListener(onRemoteConfigsUpdated); + using namespace std::chrono_literals; gameAnalytics_initialize(GAME_KEY, SECRET_KEY); diff --git a/setup.py b/setup.py index bc97e123..b5402a6f 100644 --- a/setup.py +++ b/setup.py @@ -101,6 +101,10 @@ def main(): triplet = f'{arch}-{platform}' + # 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 cmake_command += f' -DVCPKG_HOST_TRIPLET={triplet}' diff --git a/source/gameanalytics/GAState.cpp b/source/gameanalytics/GAState.cpp index d17e578d..ae6f694a 100644 --- a/source/gameanalytics/GAState.cpp +++ b/source/gameanalytics/GAState.cpp @@ -637,7 +637,8 @@ namespace gameanalytics } catch (json::exception& e) { - logging::GALogger::e(e.what()); + // discard unparseable cached config + logging::GALogger::w("Discarding incompatible cached sdk config: %s", e.what()); store::GAStore::setState("sdk_config_cached", ""); } } diff --git a/source/gameanalytics/GameAnalyticsExtern.cpp b/source/gameanalytics/GameAnalyticsExtern.cpp index a73bf901..17b3b4bb 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,50 @@ GA_API const char* gameAnalytics_getRemoteConfigsValueAsJson(const char* key) return gameAnalytics_allocString(returnValue); } +namespace +{ + 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(); diff --git a/source/gameanalytics/Platform/GALinux.cpp b/source/gameanalytics/Platform/GALinux.cpp index 56dad87e..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; } }