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:
- Size. The SDK (plus its bundled SQLite/zlib) is duplicated once per module.
- 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.
The SDK ships a flat C ABI in 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 withoutdllimport, so the C API works across the boundary regardless. Consumers that link the sharedMSTelemetry::mattarget do getdllimportapplied automatically (via theMATSDK_IMPORT_LIBinterface 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.
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.
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:
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.
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.
The SDK has no .def file; on Windows, exporting/importing is driven entirely by
MATSDK_LIBABI in ctmacros.hpp, which the
build ties to the actual linkage:
- Shared build: the SDK is compiled with
MATSDK_SHARED_LIB(__declspec(dllexport)), and the installedMSTelemetry::mattarget carries anINTERFACEdefinition ofMATSDK_IMPORT_LIB, so consumers thatfind_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.
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, orevt_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.
Place a single runtime where every module finds it:
- Windows: the same directory as the consumers (or side-by-side assembly).
- Linux:
RPATH=$ORIGINso 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.
- Dependency present, definitions absent.
dumpbin /dependents(Windows),ldd(Linux),otool -L(macOS) on each consumer should show a dependency on the onematmodule;dumpbin /exports(ornm -D) on a consumer should show it imports — not defines — the SDK symbols. - One copy at runtime. Process Explorer /
/proc/<pid>/maps/vmmapshould map thematmodule exactly once; there should be one offline-cache file. - (C++ option) CRT/STL smoke test. Have a consumer pass a
std::stringevent property into the SDK and read it back. A/MDvs/MTor_ITERATOR_DEBUG_LEVELmismatch typically crashes immediately (especially in Debug).