diff --git a/.github/workflows/vcpkg-release-bump.yml b/.github/workflows/vcpkg-release-bump.yml new file mode 100644 index 000000000..d09706242 --- /dev/null +++ b/.github/workflows/vcpkg-release-bump.yml @@ -0,0 +1,193 @@ +name: Vcpkg release bump + +# Opens a version-bump pull request against microsoft/vcpkg for the +# `cpp-client-telemetry` port whenever a new SDK release is published. +# +# It runs ONLY when a new version is cut: +# * automatically on a published, non-draft, non-prerelease GitHub Release +# whose tag looks like a version (vMAJOR.MINOR.PATCH.BUILD), or +# * manually via workflow_dispatch for a specific tag (recovery / re-run). +# It never runs on ordinary pushes, and it opens no PR if the port already +# matches the release (no version change). +# +# One-time setup required in this repository: +# * Variable VCPKG_FORK_REPO -> the vcpkg fork to push branches to, +# e.g. "your-org/vcpkg". +# * Secret VCPKG_BUMP_TOKEN -> a PAT (classic: repo+workflow, or +# fine-grained: Contents+Pull requests RW on +# the fork) able to push to VCPKG_FORK_REPO and +# open pull requests on microsoft/vcpkg. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Release tag to bump the vcpkg port to (e.g. v3.10.161.1)" + required: true + type: string + +permissions: + contents: read + +concurrency: + group: vcpkg-release-bump-${{ github.event.release.tag_name || github.event.inputs.tag }} + cancel-in-progress: false + +jobs: + bump: + name: Bump cpp-client-telemetry port + # Skip drafts and pre-releases; always allow manual dispatch. + if: >- + ${{ github.event_name == 'workflow_dispatch' || + (github.event.release.draft == false && github.event.release.prerelease == false) }} + runs-on: ubuntu-latest + env: + UPSTREAM_REPO: ${{ github.repository }} # microsoft/cpp_client_telemetry + VCPKG_UPSTREAM: microsoft/vcpkg + VCPKG_FORK_REPO: ${{ vars.VCPKG_FORK_REPO }} + PORT: cpp-client-telemetry + steps: + - name: Validate configuration + env: + VCPKG_BUMP_TOKEN: ${{ secrets.VCPKG_BUMP_TOKEN }} + run: | + set -euo pipefail + if [ -z "${VCPKG_FORK_REPO}" ]; then + echo "::error::Repository variable VCPKG_FORK_REPO is not set (e.g. 'your-org/vcpkg')." + exit 1 + fi + if [ -z "${VCPKG_BUMP_TOKEN}" ]; then + echo "::error::Secret VCPKG_BUMP_TOKEN is not set. Provide a token that can push to ${VCPKG_FORK_REPO} and open PRs on ${VCPKG_UPSTREAM}." + exit 1 + fi + + - name: Resolve tag and version + id: ver + env: + # Pass untrusted tag values through the environment instead of + # interpolating ${{ ... }} directly into the script body, so a tag + # containing shell metacharacters cannot inject commands into this + # step (which shares a runner with later PAT-bearing steps). + RELEASE_TAG: ${{ github.event.release.tag_name }} + INPUT_TAG: ${{ github.event.inputs.tag }} + run: | + set -euo pipefail + TAG="${RELEASE_TAG:-$INPUT_TAG}" + if [ -z "${TAG}" ]; then echo "::error::No release tag could be resolved."; exit 1; fi + # Only act on version tags: vMAJOR.MINOR.PATCH.BUILD. A non-matching + # tag from the automatic release trigger is a clean no-op (the SDK also + # has historical 3-part tags such as v3.3.8); a non-matching tag from a + # manual workflow_dispatch is user error and fails loudly. + if ! printf '%s' "${TAG}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "::error::Tag '${TAG}' is not a version tag (expected vX.Y.Z.W)." + exit 1 + fi + echo "::notice::Tag '${TAG}' is not a version tag (expected vX.Y.Z.W); nothing to bump." + echo "skip=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + VERSION="${TAG#v}" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "branch=port/${PORT}-${VERSION}" >> "$GITHUB_OUTPUT" + echo "Bumping ${PORT} -> tag=${TAG} version=${VERSION}" + + - name: Compute source archive SHA512 + id: sha + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + URL="https://github.com/${UPSTREAM_REPO}/archive/${{ steps.ver.outputs.tag }}.tar.gz" + echo "Downloading ${URL}" + curl -fsSL --retry 3 "${URL}" -o source.tar.gz + SHA512="$(sha512sum source.tar.gz | cut -d' ' -f1)" + echo "sha512=${SHA512}" >> "$GITHUB_OUTPUT" + echo "SHA512=${SHA512}" + + - name: Clone vcpkg fork and branch off upstream master + if: ${{ steps.ver.outputs.skip != 'true' }} + env: + GH_TOKEN: ${{ secrets.VCPKG_BUMP_TOKEN }} + run: | + set -euo pipefail + # Authenticate git via gh's credential helper instead of embedding the + # token in the clone URL (which would persist it in .git/config and + # risk leaking it if git echoes the remote). The helper is written to + # the global gitconfig and reused by the later push step. + gh auth setup-git + git clone --depth 1 "https://github.com/${VCPKG_FORK_REPO}.git" vcpkg + cd vcpkg + git remote add upstream "https://github.com/${VCPKG_UPSTREAM}.git" + git fetch --depth 1 upstream master + git checkout -B "${{ steps.ver.outputs.branch }}" upstream/master + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Bootstrap vcpkg + if: ${{ steps.ver.outputs.skip != 'true' }} + run: cd vcpkg && ./bootstrap-vcpkg.sh -disableMetrics + + - name: Update port REF, SHA512 and version + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + cd vcpkg + PORTFILE="ports/${PORT}/portfile.cmake" + MANIFEST="ports/${PORT}/vcpkg.json" + if [ ! -f "${PORTFILE}" ] || [ ! -f "${MANIFEST}" ]; then + echo "::error::${PORT} port not found in ${VCPKG_UPSTREAM}. The port must already be in the registry before it can be bumped." + exit 1 + fi + sed -i -E "s|^([[:space:]]*REF[[:space:]]+).*$|\1${{ steps.ver.outputs.tag }}|" "${PORTFILE}" + sed -i -E "s|^([[:space:]]*SHA512[[:space:]]+).*$|\1${{ steps.sha.outputs.sha512 }}|" "${PORTFILE}" + jq --arg v "${{ steps.ver.outputs.version }}" '.version = $v | del(."port-version")' "${MANIFEST}" > "${MANIFEST}.tmp" + mv "${MANIFEST}.tmp" "${MANIFEST}" + ./vcpkg format-manifest "${MANIFEST}" + + - name: Detect change + id: diff + if: ${{ steps.ver.outputs.skip != 'true' }} + run: | + set -euo pipefail + cd vcpkg + if git diff --quiet -- "ports/${PORT}"; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No change: ${PORT} is already at ${{ steps.ver.outputs.version }} with this REF/SHA512. Nothing to do." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Commit, update version DB, push and open PR + if: ${{ steps.ver.outputs.skip != 'true' && steps.diff.outputs.changed == 'true' }} + env: + GH_TOKEN: ${{ secrets.VCPKG_BUMP_TOKEN }} + run: | + set -euo pipefail + cd vcpkg + # gh auth setup-git ran in the clone step; reuse that credential helper + # so 'git push' authenticates without a token in the remote URL. + BR="${{ steps.ver.outputs.branch }}" + git add "ports/${PORT}" + git commit -m "[${PORT}] Update to ${{ steps.ver.outputs.version }}" + ./vcpkg x-add-version "${PORT}" --overwrite-version + git add versions + git commit -m "[${PORT}] Update version database" + # Ensure a remote-tracking ref exists so --force-with-lease has a lease + # to compare against on reruns: the bump branch may already exist on the + # fork but be absent from this fresh clone. Ignore failure on the first + # run, when the branch does not exist remotely yet. + git fetch origin "+refs/heads/${BR}:refs/remotes/origin/${BR}" || true + git push --force-with-lease origin "${BR}" + if [ -n "$(gh pr list --repo "${VCPKG_UPSTREAM}" --head "$(printf '%s' "${VCPKG_FORK_REPO}" | cut -d/ -f1):${BR}" --state open --json number --jq '.[0].number // empty' 2>/dev/null)" ]; then + echo "An open PR already exists for ${BR}; the force-pushed branch refreshes it." + else + gh pr create \ + --repo "${VCPKG_UPSTREAM}" \ + --base master \ + --head "$(printf '%s' "${VCPKG_FORK_REPO}" | cut -d/ -f1):${BR}" \ + --title "[${PORT}] Update to ${{ steps.ver.outputs.version }}" \ + --body "Automated port bump to [\`${UPSTREAM_REPO}@${{ steps.ver.outputs.tag }}\`](https://github.com/${UPSTREAM_REPO}/releases/tag/${{ steps.ver.outputs.tag }}). Generated by the \`vcpkg-release-bump\` workflow." + fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a0ba0e82..360bf7436 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -149,16 +149,17 @@ else() endif() if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") - # Using GCC with -s and -Wl linker flags - set(REL_FLAGS "-s -Wl,--gc-sections -Os ${WARN_FLAGS} -ffunction-sections -fdata-sections -fmerge-all-constants") + # Using GCC with -s and -Wl linker flags. -ffunction-sections/-fdata-sections + # are set once for all dep modes by the global block further below. + set(REL_FLAGS "-s -Wl,--gc-sections -Os ${WARN_FLAGS} -fmerge-all-constants") elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "MSVC") set(REL_FLAGS "${WARN_FLAGS}") elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") - # AppleClang does not support -ffunction-sections and -fdata-sections with the -fembed-bitcode and -fembed-bitcode-marker set(REL_FLAGS "-Os ${WARN_FLAGS} -fmerge-all-constants") else() - # Using clang - strip unsupported GCC options - set(REL_FLAGS "-Os ${WARN_FLAGS} -ffunction-sections -fmerge-all-constants") + # Using clang - strip unsupported GCC options (-ffunction-sections is set by + # the global block further below). + set(REL_FLAGS "-Os ${WARN_FLAGS} -fmerge-all-constants") endif() ## Uncomment this to reduce the volume of note warnings on RPi4 w/gcc-8 Ref. https://gcc.gnu.org/ml/gcc/2017-05/msg00073.html @@ -206,6 +207,55 @@ endif() endif() # NOT MATSDK_USE_VCPKG_DEPS (compiler flags) +# --- Dead-strip enablement (applies in BOTH vendored and vcpkg modes) --------- +# Deliberate exception to the "let the toolchain manage compiler flags" note +# above (the NOT MATSDK_USE_VCPKG_DEPS block): these flags are NOT optimization +# or dependency choices the vcpkg toolchain owns -- they only split functions and +# data into separate COMDATs/sections so a *consumer's* linker can drop +# unreferenced SDK code (MSVC /OPT:REF + /OPT:ICF, GNU/Clang --gc-sections, Apple +# ld -dead_strip). The toolchain does not set them, and the vcpkg-packaged +# library (and every MSVC build, which never gets /Gy from the block above) would +# otherwise link whole .obj files instead of individual functions. Applying them +# here in both modes closes that gap and matches the MSBuild Release projects, +# which already enable FunctionLevelLinking + OptimizeReferences + COMDATFolding. +if(MSVC) + # /Gy (function-level linking) is supported by both cl.exe and clang-cl. + add_compile_options(/Gy) + # /Gw (whole-program global data) is cl.exe-only; the ClangCL toolset (for + # which MSVC is also true) does not support it. + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_compile_options(/Gw) + endif() +elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang") + # On Mach-O, clang emits .subsections_via_symbols, so ld64's -dead_strip + # already removes unreferenced code at per-symbol (function) granularity + # without -ffunction-sections; we add it only for cross-toolchain + # consistency. -fdata-sections is omitted because it historically conflicted + # with bitcode on AppleClang. + add_compile_options(-ffunction-sections) +else() + # GCC / Clang (Linux, Android, MinGW) + add_compile_options(-ffunction-sections -fdata-sections) +endif() + +# Hidden symbol visibility (non-Windows): export only the MATSDK_LIBABI-decorated +# public API (classes + the C API), hiding SDK internals and the bundled +# sqlite3/zlib. This shrinks the dynamic symbol table (faster dynamic +# linking/loading, smaller binaries) and enables more inlining + dead-code +# elimination -- the non-Windows analog of what /Gy plus the consumer's /OPT:REF +# achieve on MSVC. All Windows toolchains (MSVC, MinGW, ClangCL) restrict exports +# via __declspec(dllexport) on MATSDK_LIBABI (lib/include/public/ctmacros.hpp), +# so this is gated on NOT WIN32 (not NOT MSVC, which would also catch MinGW/ +# Clang-GNU Windows builds and apply ELF-style visibility that does not belong on +# a PE/COFF target). +if(NOT WIN32) + # -fvisibility=hidden applies to C and C++; -fvisibility-inlines-hidden is a + # C++-only option, so scope it to CXX. (Applying it to C sources -- e.g. the + # bundled sqlite3/zlib on the legacy Android path -- makes Clang emit an + # "unused argument" warning that becomes an error under the project's -Werror.) + add_compile_options(-fvisibility=hidden $<$:-fvisibility-inlines-hidden>) +endif() + include(tools/Utils.cmake) include(GNUInstallDirs) include(CMakePackageConfigHelpers) diff --git a/docs/building-with-vcpkg.md b/docs/building-with-vcpkg.md index b736ba3c5..7e5cff7d0 100644 --- a/docs/building-with-vcpkg.md +++ b/docs/building-with-vcpkg.md @@ -1,6 +1,6 @@ # Building 1DS C++ SDK with vcpkg -[vcpkg](https://vcpkg.io/) is a Microsoft cross-platform open source C++ package manager. Onboarding instructions for Windows, Linux and Mac OS X [available here](https://docs.microsoft.com/en-us/cpp/build/vcpkg). This document assumes that the customer build system is already configured to use vcpkg ([getting started guide](https://learn.microsoft.com/en-us/vcpkg/get_started/overview)). 1DS C++ SDK maintainers provide a build recipe, `cpp-client-telemetry` port or CONTROL file for vcpkg. The mainline vcpkg repo is refreshed to point to latest stable open source release of 1DS C++ SDK. +[vcpkg](https://vcpkg.io/) is a Microsoft cross-platform open source C++ package manager. Onboarding instructions for Windows, Linux and Mac OS X [available here](https://docs.microsoft.com/en-us/cpp/build/vcpkg). This document assumes that the customer build system is already configured to use vcpkg ([getting started guide](https://learn.microsoft.com/en-us/vcpkg/get_started/overview)). The `cpp-client-telemetry` port is published in the official vcpkg registry, so it can be consumed directly with no overlay or extra configuration. Maintainers refresh the registry to point to the latest stable open source release of the 1DS C++ SDK on each release. The port provides the core SDK — the `MSTelemetry::mat` target and its public C++ headers. The optional Microsoft-proprietary modules (Privacy Guard, @@ -16,7 +16,8 @@ git clone --recurse-submodules https://github.com/microsoft/cpp_client_telemetry ### Installing from the vcpkg registry -Once a new port has been accepted into the official vcpkg registry, install with: +The `cpp-client-telemetry` port is available in the [official vcpkg registry](https://github.com/microsoft/vcpkg/tree/master/ports/cpp-client-telemetry), +so you can install it directly — no overlay or extra configuration required: ```console vcpkg install cpp-client-telemetry @@ -26,8 +27,9 @@ That's it! The package should be compiled for the current OS. ### Installing from the overlay port (development / pre-release) -Before the port is published, or to test local changes, use the overlay port -shipped in this repository: +The overlay port shipped in this repository is for **development only** — use it +to test local changes to the port, or a newer SDK revision, before they are +published to the registry: ```console git clone https://github.com/microsoft/cpp_client_telemetry @@ -190,6 +192,70 @@ will automatically use the optimized zlib-ng build. > zlib. When using `ZLIB_COMPAT=ON`, ensure all dependencies resolve to > zlib-ng rather than mixing stock zlib and zlib-ng. +## Reducing binary footprint + +This section applies when the SDK is linked **statically** into your binary +(the default for the `*-static` vcpkg triplets) — most footprint control then +lives on *your* side of the link. If you instead consume a **dynamic** `mat` +(e.g. the default `x64-windows` triplet, or `BUILD_SHARED_LIBS=ON`), the runtime +ships as its own `mat.dll` / `libmat.so` / `libmat.dylib`; the SDK's own +`-fvisibility=hidden` and `/Gy /Gw` already trim its exported symbol table, and +the consumer-side linker options below are specific to the static-link case. + +### Enable linker dead-stripping (largest lever) + +The SDK is compiled with function-level linking (`/Gy /Gw` on MSVC, +`-ffunction-sections -fdata-sections` on GCC/Clang) so that **your** linker can +discard SDK code you never reference. Make sure your final link enables it: + +- **MSVC:** `/OPT:REF` (drop unreferenced functions/data) and `/OPT:ICF` (fold + identical COMDATs). These are on by default for Release, **but `/DEBUG` flips + their default to off** (`/OPT:NOREF,NOICF`, per the MSVC `/OPT` docs) — so if + you ship PDBs, re-enable them explicitly. `/OPT:REF` is also incompatible with + incremental linking, so set `/INCREMENTAL:NO`: + + ```cmake + target_link_options(your_target PRIVATE + $<$,$>:/OPT:REF> + $<$,$>:/OPT:ICF> + $<$,$>:/INCREMENTAL:NO>) + ``` + +- **GCC / Clang:** link with `-Wl,--gc-sections`. +- **Apple (clang):** link with `-Wl,-dead_strip`. + +This is by far the largest lever — on a static `x64-windows-static` Release link +it can roughly halve the binary. The SDK's `/Gy /Gw` flags only *enable* this; +the stripping happens at your link. Keep the SDK a static dependency linked +*into* your binary: if you re-export its API across your own DLL boundary, the +export table pins its symbols and defeats `/OPT:REF`. + +### Drop unused SQLite features (json1) + +The SDK uses SQLite only for offline event storage — plain tables and indexes, +with no JSON, FTS, R*Tree, or virtual-table features. This in-repo overlay port +already requests `sqlite3` with `default-features: false` on its dependency edge +(the published registry port will follow once this change is upstreamed). + +vcpkg unions feature requests across the whole dependency graph, and a +transitive opt-out alone is **not** enough: you must **also** request `sqlite3` +with `default-features: false` in your own top-level manifest to actually omit +`json1` (which compiles SQLite with `SQLITE_OMIT_JSON`, ~50 KB smaller on a +static `x64-windows-static` Release build): + +```json +{ + "dependencies": [ + "cpp-client-telemetry", + { "name": "sqlite3", "default-features": false } + ] +} +``` + +If any package in your build (or your own code) needs SQLite's JSON functions, +request `sqlite3[json1]` instead and the extension is restored for the whole +graph. + ## How It Works: MATSDK_USE_VCPKG_DEPS When the SDK detects it is being built via vcpkg (by checking for diff --git a/docs/sharing-a-single-sdk-runtime.md b/docs/sharing-a-single-sdk-runtime.md new file mode 100644 index 000000000..10e69746d --- /dev/null +++ b/docs/sharing-a-single-sdk-runtime.md @@ -0,0 +1,164 @@ +# Sharing one SDK runtime across several modules in a process + +When more than one module in a single process links this SDK — for example an +application that loads several plug-ins or libraries, each of which uses 1DS — +the easy default (every module statically embeds the SDK) has two costs: + +1. **Size.** The SDK (plus its bundled SQLite/zlib) is duplicated once per module. +2. **Duplicated global state.** Each static copy has its *own* default + `LogManager`, HTTP transport, offline SQLite cache, and upload threads. They do + not share a pipeline, and multiple writers to the same offline-cache path will + corrupt it. + +This document describes how to ship **one** shared SDK runtime (`mat.dll` / +`libmat.so` / `libmat.dylib`) that every module imports, so there is a single +copy on disk and a single set of process-global state. + +There are two ways to consume the shared runtime. **The C API is strongly +recommended** because it removes the fragile C++/CRT ABI coupling between modules. + +--- + +## Option 1 (recommended): consume the stable C API + +The SDK ships a flat **C ABI** in [`mat.h`](../lib/include/public/mat.h). Every +`evt_*` entry point (`evt_open`, `evt_log`, `evt_flush`, `evt_upload`, +`evt_pause`, `evt_resume`, `evt_close`, `evt_configure`, …) is a `static inline` +wrapper that marshals its arguments into a POD struct and calls through a single +exported `__cdecl` symbol, `evt_api_call_default`. + +Consequences that make this the robust choice: + +* **Only one symbol crosses the module boundary, and no C++/STL type does.** The + request is a plain C struct, so there is *no* requirement that the modules and + the shared runtime agree on the C++ standard library ABI (`/MD`, + `_ITERATOR_DEBUG_LEVEL`, MSVC toolset/STL version, libstdc++ vs libc++, + `_GLIBCXX_USE_CXX11_ABI`, …). A 1DS version bump does not force every module to + rebuild in lockstep against an identical toolchain. +* **It does not *require* `__declspec(dllimport)` to link.** A plain C function + resolves through the shared library's import lib even without `dllimport`, so + the C API works across the boundary regardless. Consumers that link the shared + `MSTelemetry::mat` target do get `dllimport` applied automatically (via the + `MATSDK_IMPORT_LIB` interface define this PR adds); for a C function that is a + harmless calling-convention optimization, not a requirement. + +Each module includes `mat.h`, links the one shared runtime, and uses its own +tenant/source. You still pin the **same SDK version** in every module (so the +request/struct layout matches), but you avoid the C++ ABI lockstep entirely. + +## Option 2: consume the C++ API from a shared library + +All modules `find_package(MSTelemetry CONFIG REQUIRED)` and link +`MSTelemetry::mat` (resolving to the import lib); none statically embed the SDK. + +The C++ public API passes C++ standard-library types (`std::string`, `std::map`, +…) across the module boundary, so **every module and the shared runtime must +share one C++ ABI**. If they do not, you get heap corruption / undefined +behavior. Pin all of the following identically: + +| Axis | Requirement | +|------|-------------| +| **CRT linkage (Windows)** | Dynamic CRT (`/MD`, `/MDd` for Debug) everywhere — never `/MT`, and never mix Debug/Release CRT across the boundary. (vcpkg: `VCPKG_CRT_LINKAGE dynamic`.) | +| **STL / iterator debug** | One compiler + STL, one build config. `_ITERATOR_DEBUG_LEVEL` must match (Release `0` vs Debug `2`) — a Release consumer + Debug runtime is a silent layout mismatch. | +| **Toolset** | One MSVC toolset across all binaries (the v14x toolsets share an STL ABI, but don't mix major versions); or one libstdc++/libc++ with the same `_GLIBCXX_USE_CXX11_ABI`. | +| **Language / model** | Same `/std:c++NN`, same `/EHsc` exception model, same architecture, no overridden struct packing. | +| **SDK build options** | Same SDK feature/version selection in every module's manifest — different features mean different headers, hence a different ABI even at the same version. | + +Because the C++ ABI must match exactly across separately built and separately +versioned modules, this option is materially more brittle than the C API. Prefer +Option 1 unless you specifically need the C++ surface and control all modules' +toolchains. + +--- + +## Building the shared runtime with vcpkg (per-port linkage) + +A common requirement is "share *this* SDK, but keep everything else statically +linked (no DLL forest)". Override the library linkage **per port** in your +triplet so only this SDK goes dynamic: + +```cmake +set(VCPKG_CRT_LINKAGE dynamic) +if(PORT STREQUAL "cpp-client-telemetry") + set(VCPKG_LIBRARY_LINKAGE dynamic) # mat.dll / libmat.so / libmat.dylib + import lib +else() + set(VCPKG_LIBRARY_LINKAGE static) +endif() +``` + +The port honors `VCPKG_LIBRARY_LINKAGE` / `BUILD_SHARED_LIBS` and emits the +shared `mat` plus its import lib and the `MSTelemetry` CMake config package. + +### Pin one version across all modules + +All modules must compile against identical SDK headers. Pin the same +`cpp-client-telemetry` version and `builtin-baseline` (or a shared version +override) in every module's manifest, and ideally build all artifacts in the same +CI job/container with the same toolchain image. Most ABI drift comes from +separate modules quietly building on different agents. + +--- + +## How the SDK decorates its public symbols + +The SDK has no `.def` file; on Windows, exporting/importing is driven entirely by +`MATSDK_LIBABI` in [`ctmacros.hpp`](../lib/include/public/ctmacros.hpp), which the +build ties to the actual linkage: + +* **Shared build:** the SDK is compiled with `MATSDK_SHARED_LIB` + (`__declspec(dllexport)`), and the installed `MSTelemetry::mat` target carries + an `INTERFACE` definition of `MATSDK_IMPORT_LIB`, so consumers that + `find_package` + link automatically get `__declspec(dllimport)` — no + consumer-side configuration required. +* **Static build:** nothing is decorated, so the SDK's public symbols are not + re-exported by a consumer DLL that absorbs the static lib. + +On non-Windows platforms the SDK is built with `-fvisibility=hidden` and the +public API is marked `__attribute__((visibility("default")))`, so only the public +API (including the C API) is exported from the shared object. + +--- + +## Coordinate the single runtime's lifetime + +One shared runtime means **one** set of process-global state. Decide ownership: + +* **Recommended — single owner.** The top-level module initializes and tears down + the SDK (`LogManager::Initialize` / `FlushAndTeardown`, or `evt_open` / + `evt_close`). Other modules obtain loggers (their own tenant/source) but never + initialize or tear down. This avoids teardown-ordering crashes. +* **Alternative — named instances.** `LogManagerProvider::CreateLogManager(id)` + gives each module its own instance/tenant/config sharing the one transport; then + you need a last-one-out teardown refcount and **distinct offline-cache paths** + (one shared path with multiple writers corrupts it). +* Teardown must happen exactly once, **last**, after every module has stopped + logging. + +--- + +## Ship exactly one copy on the loader path + +Place a single runtime where every module finds it: + +* **Windows:** the same directory as the consumers (or side-by-side assembly). +* **Linux:** `RPATH=$ORIGIN` so every module resolves the one copy. +* **macOS:** a stable install name, `@rpath/libmat.dylib`. + +Make exactly one package own and ship the SDK runtime; the others declare a +dependency rather than bundling their own. If several packages each ship their +own copy, which one loads is path-order luck — and if their versions differ, you +are back to an ABI mismatch even with "one" DLL. + +--- + +## Validate + +* **Dependency present, definitions absent.** `dumpbin /dependents` (Windows), + `ldd` (Linux), `otool -L` (macOS) on each consumer should show a dependency on + the one `mat` module; `dumpbin /exports` (or `nm -D`) on a consumer should show + it imports — not defines — the SDK symbols. +* **One copy at runtime.** Process Explorer / `/proc//maps` / `vmmap` should + map the `mat` module exactly once; there should be one offline-cache file. +* **(C++ option) CRT/STL smoke test.** Have a consumer pass a `std::string` event + property into the SDK and read it back. A `/MD` vs `/MT` or `_ITERATOR_DEBUG_LEVEL` + mismatch typically crashes immediately (especially in Debug). diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 584c678ec..0d4161811 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -216,7 +216,7 @@ if(PAL_IMPLEMENTATION STREQUAL "CPP11") endif() if(APPLE AND BUILD_OBJC_WRAPPER) message(STATUS "Include ObjC Wrappers") - list(APPEND SRCS + set(OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWLogger.mm ../wrappers/obj-c/ODWLogManager.mm ../wrappers/obj-c/ODWEventProperties.mm @@ -227,22 +227,23 @@ if(PAL_IMPLEMENTATION STREQUAL "CPP11") ../wrappers/obj-c/ODWSanitizerInitConfig.mm ) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/dataviewer/") - list(APPEND SRCS + list(APPEND OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWDiagnosticDataViewer.mm ) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/privacyguard/" AND BUILD_PRIVACYGUARD) set(MATSDK_OBJC_PRIVACYGUARD_AVAILABLE ON) - list(APPEND SRCS + list(APPEND OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWPrivacyGuard.mm ) endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/modules/sanitizer/" AND BUILD_SANITIZER) set(MATSDK_OBJC_SANITIZER_AVAILABLE ON) - list(APPEND SRCS + list(APPEND OBJC_WRAPPER_SRCS ../wrappers/obj-c/ODWSanitizer.mm ) endif() + list(APPEND SRCS ${OBJC_WRAPPER_SRCS}) endif() if(APPLE AND BUILD_SWIFT_WRAPPER) @@ -271,7 +272,7 @@ elseif(PAL_IMPLEMENTATION STREQUAL "WIN32") if(NOT MATSDK_USE_VCPKG_DEPS) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../zlib ${CMAKE_CURRENT_SOURCE_DIR}/../sqlite) endif() -add_definitions(-D_UNICODE -DUNICODE -DWIN32 -DMATSDK_PLATFORM_WINDOWS=1 -DMATSDK_SHARED_LIB=1 -D_UTC_SDK -DUSE_BOND -D_WINDOWS -D_USRDLL -DWINVER=_WIN32_WINNT_WIN7) +add_definitions(-D_UNICODE -DUNICODE -DWIN32 -DMATSDK_PLATFORM_WINDOWS=1 -D_UTC_SDK -DUSE_BOND -D_WINDOWS -D_USRDLL -DWINVER=_WIN32_WINNT_WIN7) remove_definitions(-D_MBCS) list(APPEND SRCS http/HttpClient_WinInet.cpp @@ -323,6 +324,29 @@ else() add_library(mat STATIC ${SRCS}) endif() +# Public-API export decoration (MATSDK_LIBABI in lib/include/public/ctmacros.hpp). +# The SDK has no .def file, so __declspec(dllexport)/(dllimport) on Windows and +# __attribute__((visibility("default"))) elsewhere are the sole export mechanisms, +# and the decoration must follow the actual linkage: +# * shared: the SDK's own translation units export the public API +# (MATSDK_SHARED_LIB, PRIVATE). On Windows, consumers must additionally import +# it -- the INTERFACE MATSDK_IMPORT_LIB is carried by the installed +# MSTelemetry::mat target, so find_package() + link gives consumers dllimport +# automatically with no consumer-side configuration. Non-Windows consumers need +# nothing: they call symbols exported by libmat.so/.dylib. +# * static: decorate nothing, so the SDK's public symbols are NOT re-exported by +# a consumer DLL/.so that statically absorbs this library. Windows needs an +# explicit (empty) MATSDK_STATIC_LIB; elsewhere the empty MATSDK_LIBABI default +# plus -fvisibility=hidden (root CMakeLists.txt) already hides them. +if(BUILD_SHARED_LIBS) + target_compile_definitions(mat PRIVATE MATSDK_SHARED_LIB=1) + if(WIN32) + target_compile_definitions(mat INTERFACE MATSDK_IMPORT_LIB=1) + endif() +elseif(WIN32) + target_compile_definitions(mat PUBLIC MATSDK_STATIC_LIB=1) +endif() + # Target-based include paths for vcpkg / install workflow. # PUBLIC propagates to consumers; PRIVATE is SDK-internal only. # BUILD_INTERFACE is used during the SDK build; INSTALL_INTERFACE is used @@ -339,6 +363,17 @@ target_include_directories(mat ) if(APPLE AND BUILD_OBJC_WRAPPER) + if(BUILD_SHARED_LIBS AND OBJC_WRAPPER_SRCS) + # The root CMakeLists.txt applies -fvisibility=hidden globally to shrink the + # exported symbol table of the core C++ SDK. For Objective-C that also hides + # the wrapper class symbols (_OBJC_CLASS_$_ODW*), which are public API on + # Apple: a shared libmat.dylib would export no ODW* classes and consumers + # would fail to link (undefined _OBJC_CLASS_$_...). Re-export just the wrapper + # translation units with default visibility; the C++ core stays hidden. + set_source_files_properties(${OBJC_WRAPPER_SRCS} + PROPERTIES COMPILE_FLAGS "-fvisibility=default") + endif() + if(MATSDK_OBJC_PRIVACYGUARD_AVAILABLE) target_compile_definitions(mat PRIVATE MATSDK_OBJC_PRIVACYGUARD_AVAILABLE=1) else() diff --git a/lib/include/public/ctmacros.hpp b/lib/include/public/ctmacros.hpp index 42547e41d..cabd36f5f 100644 --- a/lib/include/public/ctmacros.hpp +++ b/lib/include/public/ctmacros.hpp @@ -28,9 +28,14 @@ #define MATSDK_LIBABI_CDECL __cdecl # if defined(MATSDK_SHARED_LIB) # define MATSDK_LIBABI __declspec(dllexport) +# elif defined(MATSDK_IMPORT_LIB) +// Consumer importing the public API from a shared mat.dll. The installed +// MSTelemetry::mat CMake target propagates this automatically when the SDK was +// built shared (see lib/CMakeLists.txt). +# define MATSDK_LIBABI __declspec(dllimport) # elif defined(MATSDK_STATIC_LIB) # define MATSDK_LIBABI -# else // Header file included by client +# else // Header file included by client; linkage unspecified # ifndef MATSDK_LIBABI # define MATSDK_LIBABI # endif @@ -47,8 +52,19 @@ #define MATSDK_LIBABI_CDECL #endif -#ifndef MATSDK_LIBABI -#define MATSDK_LIBABI +#ifndef MATSDK_LIBABI +// Mark the public API as default-visibility ONLY in shared builds, so it stays +// exported when the SDK is compiled with -fvisibility=hidden (see CMakeLists.txt). +// This mirrors the __declspec(dllexport) gating above: in a static build the +// attribute is omitted, so the public symbols inherit -fvisibility=hidden and are +// NOT re-exported when a consumer .so/.dylib statically absorbs libmat. (When a +// consumer includes this header, MATSDK_SHARED_LIB is not defined either, which +// is fine: the symbols are exported by the shared libmat they link against.) +# if (defined(__GNUC__) || defined(__clang__)) && defined(MATSDK_SHARED_LIB) +# define MATSDK_LIBABI __attribute__((visibility("default"))) +# else +# define MATSDK_LIBABI +# endif #endif // TODO: [MG] - ideally we'd like to use __attribute__((unused)) with gcc/clang diff --git a/tools/ports/cpp-client-telemetry/portfile.cmake b/tools/ports/cpp-client-telemetry/portfile.cmake index b0ce77107..cfdab2368 100644 --- a/tools/ports/cpp-client-telemetry/portfile.cmake +++ b/tools/ports/cpp-client-telemetry/portfile.cmake @@ -1,8 +1,8 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO microsoft/cpp_client_telemetry - REF 4485b82005abf1d24336ace99b11df88dd578eb0 - SHA512 1f3ee1c26f1ae9e7323262c9b4c8796efba2c6addcde432d6c6c77b8c1c2f254cb8ff334b1dd0a72dc8ecfbfbae04ab374ec5ac7e5d286d6042953d53e50fd5b + REF v3.10.161.1 + SHA512 4664b34ddce601d6a95669df4a59d11a6cc67de1f23de132192f791a275edc6a10b8498d340e6cf7d120d9e7a22c494d7517b24fc0954bf9e236e84a8800589a HEAD_REF main ) @@ -46,8 +46,5 @@ vcpkg_cmake_config_fixup(PACKAGE_NAME MSTelemetry CONFIG_PATH lib/cmake/MSTeleme file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") -# Install usage instructions -file(INSTALL "${CMAKE_CURRENT_LIST_DIR}/usage" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") - # Install license vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/tools/ports/cpp-client-telemetry/usage b/tools/ports/cpp-client-telemetry/usage deleted file mode 100644 index 736d289f6..000000000 --- a/tools/ports/cpp-client-telemetry/usage +++ /dev/null @@ -1,4 +0,0 @@ -cpp-client-telemetry provides CMake targets: - - find_package(MSTelemetry CONFIG REQUIRED) - target_link_libraries(main PRIVATE MSTelemetry::mat) diff --git a/tools/ports/cpp-client-telemetry/vcpkg.json b/tools/ports/cpp-client-telemetry/vcpkg.json index d721df65a..2ee6d9bc3 100644 --- a/tools/ports/cpp-client-telemetry/vcpkg.json +++ b/tools/ports/cpp-client-telemetry/vcpkg.json @@ -6,9 +6,6 @@ "license": "Apache-2.0", "supports": "((windows & !mingw) | linux | osx | ios | android) & !uwp", "dependencies": [ - "nlohmann-json", - "sqlite3", - "zlib", { "name": "curl", "default-features": false, @@ -17,6 +14,11 @@ ], "platform": "linux | android" }, + "nlohmann-json", + { + "name": "sqlite3", + "default-features": false + }, { "name": "vcpkg-cmake", "host": true @@ -24,6 +26,7 @@ { "name": "vcpkg-cmake-config", "host": true - } + }, + "zlib" ] }