From ec3f2987c1dd11e10dfde5a32719f5daf5a48674 Mon Sep 17 00:00:00 2001 From: Dmitry Nikolaev Date: Wed, 8 Apr 2026 12:18:29 +0200 Subject: [PATCH] Implement C++11 TLS simulation Signed-off-by: Dmitry Nikolaev --- include/zos-tls-cxx.h | 123 +++++++++++++++++ test/test-tls-cxx.cc | 297 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 420 insertions(+) create mode 100644 include/zos-tls-cxx.h create mode 100644 test/test-tls-cxx.cc diff --git a/include/zos-tls-cxx.h b/include/zos-tls-cxx.h new file mode 100644 index 0000000..d3e299a --- /dev/null +++ b/include/zos-tls-cxx.h @@ -0,0 +1,123 @@ +/////////////////////////////////////////////////////////////////////////////// +// Licensed Materials - Property of IBM +// ZOSLIB +// (C) Copyright IBM Corp. 2026. All Rights Reserved. +// US Government Users Restricted Rights - Use, duplication +// or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. +/////////////////////////////////////////////////////////////////////////////// + +// TODO: revisit when `thread_local` is fully implemented on z/OS. + +/* + * Thread-local storage for z/OS (C++11). + * + * The latest LLVM based compilers on z/OS (such as Open XL C/C++ 2.2) still + * do not fully support the C++ `thread_local` keyword: only trivially + * destructible variables. + * + * The existing simulation `__tlssim` doesn't work with such variables either. + * Also, it doesn't support initialization per thread. + * + * __tls addresses the issues assuming it's used to port C++11 code (or + * higher level) in the most seamless way possible. + * + * + * Usage examples: + * + * static __tls tls_counter; + * auto &counter = tls_counter.get([]{ return 0; }); + * counter++; + * + * static __tls tls_name; + * auto &name = tls_name.get([]{ return std::string("default"); }); + * name = "updated"; + */ + +#ifndef ZOS_TLS_CXX_H_ +#define ZOS_TLS_CXX_H_ + +#if !defined(__cplusplus) || __cplusplus < 201103L +#error "zos-tls-cxx.h requires C++11 or later" +#endif + +#include "zos-macros.h" + +#include +#include +#include +#include +#include + +template class __Z_EXPORT __tls { + + std::once_flag once; + pthread_key_t key; + std::atomic key_created; + + /// Destructor callback registered with pthread_key_create. + static void destroy(void *ptr) noexcept { delete static_cast(ptr); } + + /// Create the pthread key exactly once + void ensure_key() { + std::call_once(once, [this]() { + const int rc = pthread_key_create(&key, &destroy); + if (rc != 0) + throw std::system_error(rc, std::generic_category(), + "pthread_key_create"); + key_created.store(true, std::memory_order_release); + }); + } + + /// Get raw per-thread pointer (nullptr if not set yet). + T *raw() const noexcept { return static_cast(pthread_getspecific(key)); } + + /// Install a newly-allocated T* + T &install(T *p) { + const int rc = pthread_setspecific(key, p); + if (rc != 0) { + delete p; + throw std::system_error(rc, std::generic_category(), + "pthread_setspecific"); + } + return *p; + } + +public: + __tls() noexcept : key_created(false) {} + + // It shouldn't be copyable or movable. + __tls(const __tls &) = delete; + __tls &operator=(const __tls &) = delete; + __tls(__tls &&) = delete; + __tls &operator=(__tls &&) = delete; + + /// Note: it's expected to be static + ~__tls() { + if (key_created.load(std::memory_order_acquire)) { + T *p = raw(); + if (p) { + delete p; + pthread_setspecific(key, nullptr); + } + pthread_key_delete(key); + } + } + + /// Get a reference to the calling thread's T, default-constructing it + /// on first access. + T &get() { + ensure_key(); + T *p = raw(); + return p ? *p : install(new T()); + } + + /// Get a reference to the calling thread's T, initializing it with init() on + /// first access. + template T &get(Init &&init) { + ensure_key(); + T *p = raw(); + return p ? *p : install(new T(std::forward(init)())); + } +}; + +#endif // ZOS_TLS_CXX_H_ diff --git a/test/test-tls-cxx.cc b/test/test-tls-cxx.cc new file mode 100644 index 0000000..498fb06 --- /dev/null +++ b/test/test-tls-cxx.cc @@ -0,0 +1,297 @@ +/////////////////////////////////////////////////////////////////////////////// +// Licensed Materials - Property of IBM +// ZOSLIB +// (C) Copyright IBM Corp. 2026. All Rights Reserved. +// US Government Users Restricted Rights - Use, duplication +// or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. +/////////////////////////////////////////////////////////////////////////////// + +// Tests for __tls — C++11 thread-local storage for z/OS. + +#if defined(__cplusplus) && __cplusplus >= 201103L + +#include "zos-tls-cxx.h" +#include "gtest/gtest.h" + +#include +#include + +namespace { + +/// Run a callable on a new thread and join it. +template +void run_on_thread(Fn &&fn) { + struct Ctx { + Fn *fn; + }; + Ctx ctx{&fn}; + + pthread_t tid; + int rc = pthread_create( + &tid, nullptr, + +[](void *arg) -> void * { + auto *c = static_cast(arg); + (*c->fn)(); + return nullptr; + }, + &ctx); + ASSERT_EQ(rc, 0) << "pthread_create failed"; + rc = pthread_join(tid, nullptr); + ASSERT_EQ(rc, 0) << "pthread_join failed"; +} + +/// Track construction / destruction for leak detection. +struct Tracked { + static std::atomic counter; + int value; + + explicit Tracked(int v = 0) : value(v) { counter.fetch_add(1); } + + Tracked(Tracked &&o) noexcept : value(o.value) { + o.value = -1; + counter.fetch_add(1); + } + + Tracked(const Tracked &) = delete; + Tracked &operator=(const Tracked &) = delete; + Tracked &operator=(Tracked &&) = delete; + + ~Tracked() { counter.fetch_sub(1); } +}; + +std::atomic Tracked::counter{0}; + + +TEST(TlsTest, DefaultConstruction) { + static __tls tls; + auto &v = tls.get(); + EXPECT_EQ(v, 0); +} + +TEST(TlsTest, DefaultConstructionString) { + static __tls tls; + auto &v = tls.get(); + EXPECT_TRUE(v.empty()); +} + +TEST(TlsTest, CallableInit) { + static __tls tls; + auto &v = tls.get([] { return 42; }); + EXPECT_EQ(v, 42); +} + +TEST(TlsTest, CallableInitString) { + static __tls tls; + auto &v = tls.get([] { return std::string("42"); }); + EXPECT_EQ(v, "42"); +} + +TEST(TlsTest, CallableInvokedOnce) { + static __tls tls; + int call_count = 0; + auto init = [&call_count] { + ++call_count; + return 7; + }; + + auto &v1 = tls.get(init); + auto &v2 = tls.get(init); + auto &v3 = tls.get(init); + + EXPECT_EQ(call_count, 1); + EXPECT_EQ(v1, 7); + EXPECT_EQ(&v1, &v2); + EXPECT_EQ(&v2, &v3); +} + +TEST(TlsTest, AssignmentThroughReference) { + static __tls tls; + auto &v = tls.get([] { return 1; }); + EXPECT_EQ(v, 1); + + v = 42; + EXPECT_EQ(v, 42); + + auto &v2 = tls.get([] { return 1; }); + EXPECT_EQ(v2, 42); + EXPECT_EQ(&v, &v2); +} + +TEST(TlsTest, AssignmentString) { + static __tls tls; + auto &v = tls.get([] { return std::string("initial"); }); + EXPECT_EQ(v, "initial"); + + v = "updated"; + EXPECT_EQ(v, "updated"); + + v += "!"; + EXPECT_EQ(tls.get([] { return std::string("ignored"); }), "updated!"); +} + +TEST(TlsTest, ThreadIsolation) { + static __tls tls; + auto &main_val = tls.get([] { return 100; }); + main_val = 100; + + std::atomic child_observed{-1}; + + run_on_thread([&] { + auto &child_val = tls.get([] { return 200; }); + child_observed.store(child_val); + child_val = 300; + }); + + EXPECT_EQ(child_observed.load(), 200); + EXPECT_EQ(main_val, 100); +} + +TEST(TlsTest, ThreadsIndependent) { + static __tls tls; + + constexpr int threads_num = 8; + std::atomic results[threads_num]; + for (int i = 0; i < threads_num; ++i) { + results[i].store(-1); + } + + pthread_t threads[threads_num]; + struct Args { + int id; + std::atomic *result; + }; + Args args[threads_num]; + + for (int i = 0; i < threads_num; ++i) { + args[i] = {i, &results[i]}; + pthread_create( + &threads[i], nullptr, + +[](void *arg) -> void * { + auto *a = static_cast(arg); + int id = a->id; + auto &v = tls.get([id] { return id * 10; }); + v += 1; + a->result->store(v); + return nullptr; + }, + &args[i]); + } + + for (int i = 0; i < threads_num; ++i) + pthread_join(threads[i], nullptr); + + for (int i = 0; i < threads_num; ++i) + EXPECT_EQ(results[i].load(), i * 10 + 1) << "thread " << i; +} + +TEST(TlsTest, DestructorOnThreadExit) { + int before = Tracked::counter.load(); + + { + static __tls tls; + run_on_thread([&] { + tls.get([] { return Tracked(42); }); + }); + } + + EXPECT_EQ(Tracked::counter.load(), before); +} + +TEST(TlsTest, DestructorOnAnchorDestruction) { + int before = Tracked::counter.load(); + + { + __tls tls; + tls.get([] { return Tracked(99); }); + EXPECT_EQ(Tracked::counter.load(), before + 1); + } + + EXPECT_EQ(Tracked::counter.load(), before); +} + +TEST(TlsTest, Vector) { + static __tls> tls; + auto &vec = tls.get([] { return std::vector{1, 2, 3}; }); + ASSERT_EQ(vec.size(), 3u); + EXPECT_EQ(vec[0], 1); + + vec.push_back(4); + EXPECT_EQ(vec.size(), 4u); + + auto &vec2 = tls.get([] { return std::vector{}; }); + EXPECT_EQ(vec2.size(), 4u); + EXPECT_EQ(&vec, &vec2); +} + +TEST(TlsTest, UniquePtr) { + const int before = Tracked::counter.load(); + + { + __tls> tls; + auto &ptr = tls.get([] { + return std::unique_ptr(new Tracked(123)); + }); + + ASSERT_NE(ptr, nullptr); + EXPECT_EQ(ptr->value, 123); + EXPECT_EQ(Tracked::counter.load(), before + 1); + + ptr = std::unique_ptr(new Tracked(456)); + EXPECT_EQ(ptr->value, 456); + EXPECT_EQ(Tracked::counter.load(), before + 1); + } + + EXPECT_EQ(Tracked::counter.load(), before); +} + +TEST(TlsTest, StressManyThreads) { + static __tls tls; + const int before = Tracked::counter.load(); + + constexpr int threads_num = 32; + pthread_t threads[threads_num]; + + for (int i = 0; i < threads_num; ++i) { + pthread_create( + &threads[i], nullptr, + +[](void *) -> void * { + auto &v = tls.get([] { return Tracked(1); }); + v.value += 1; + return nullptr; + }, + nullptr); + } + for (int i = 0; i < threads_num; ++i) + pthread_join(threads[i], nullptr); + + EXPECT_EQ(Tracked::counter.load(), before); +} + +TEST(TlsTest, DefaultGet) { + __tls tls; + auto &v = tls.get(); + EXPECT_TRUE(v.empty()); + + v = "value"; + EXPECT_EQ(tls.get(), "value"); +} + +TEST(TlsTest, MultipleAnchors) { + static __tls tls_a; + static __tls tls_b; + + auto &a = tls_a.get([] { return 1; }); + auto &b = tls_b.get([] { return 2; }); + + EXPECT_EQ(a, 1); + EXPECT_EQ(b, 2); + EXPECT_NE(&a, &b); + + a = 10; + EXPECT_EQ(a, 10); + EXPECT_EQ(b, 2); +} + +} // namespace + +#endif // __cplusplus >= 201103L