SiddiqSoft.RWLEnvelope 1.5.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package SiddiqSoft.RWLEnvelope --version 1.5.0
                    
NuGet\Install-Package SiddiqSoft.RWLEnvelope -Version 1.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SiddiqSoft.RWLEnvelope" Version="1.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SiddiqSoft.RWLEnvelope" Version="1.5.0" />
                    
Directory.Packages.props
<PackageReference Include="SiddiqSoft.RWLEnvelope" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SiddiqSoft.RWLEnvelope --version 1.5.0
                    
#r "nuget: SiddiqSoft.RWLEnvelope, 1.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SiddiqSoft.RWLEnvelope@1.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SiddiqSoft.RWLEnvelope&version=1.5.0
                    
Install as a Cake Addin
#tool nuget:?package=SiddiqSoft.RWLEnvelope&version=1.5.0
                    
Install as a Cake Tool

RWLEnvelope : A simple read-writer lock envelope

CodeQL Build Status alternate text is missing from this package README image alternate text is missing from this package README image alternate text is missing from this package README image alternate text is missing from this package README image

Objective

RWLEnvelope is a header-only C++ template library that provides a simple, convenient envelope-access model for thread-safe access to objects using reader-writer locks. Our goal is to:

  • Avoid re-implementing the rw-lock: The standard C++ library (since C++14) provides excellent reader-writer lock implementations via std::shared_mutex, std::unique_lock, and std::shared_lock.

  • Simplify thread-safe access patterns: We provide a convenient layer that makes it easy to work with shared data in multi-threaded applications without exposing the complexity of manual lock management.

  • Enable safe concurrent access: Support multiple concurrent readers while ensuring exclusive access for writers, with automatic lock management and RAII semantics.

  • Minimize boilerplate code: Reduce the amount of locking code needed to safely access shared objects through intuitive APIs.

<p align="right" width="50%"> <b>WE DO NOT IMPLEMENT</b> a read-writer lock; the standard C++ library has one.<br/>We provide a header-only package simplifying the locking code around thread-safe access to your underlying type. <br/> <i>NOT a wrapper; an envelope.</i> </p>

Why RWLEnvelope?

The Problem with Manual Lock Management

Writing thread-safe code is tedious. Without RWLEnvelope, you'd need to:

// Without RWLEnvelope - verbose
std::shared_mutex          mutex;
std::map<std::string, int> data;

// Reading
{
    std::shared_lock lock(mutex);
    auto it = data.find("key");
    if (it != data.end()) {
        std::cout << it->second << std::endl;
    }
} // Lock released here

// Writing
{
    std::unique_lock lock(mutex);
    data["key"] = 42;
} // Lock released here

The RWLEnvelope Solution

With RWLEnvelope, the same code becomes cleaner:

// With RWLEnvelope - clean and safe
siddiqsoft::RWLEnvelope<std::map<std::string, int>> data;

// Reading
data.observe([](const auto& m) noexcept {
    auto it = m.find("key");
    if (it != m.end()) {
        std::cout << it->second << std::endl;
    }
});

// Writing
data.mutate([](auto& m) noexcept {
    m["key"] = 42;
});

Key Benefits

  1. Automatic Lock Management: Locks are acquired and released automatically via RAII. No risk of forgetting to unlock.

  2. Clear Intent: observe() for reads and mutate() for writes makes your code's intent explicit and self-documenting.

  3. Reduced Boilerplate: No need to manually create lock objects or manage scopes. The library handles it.

  4. Type Safety: The template enforces that you're working with the correct type. No accidental type mismatches.

  5. Exception Safe: If your callback throws, the lock is still released properly. No deadlocks or resource leaks.

  6. Flexible Access Patterns: Choose between callback-based access (for simple operations) or direct lock access (for complex operations).

  7. Zero Overhead: Header-only implementation with no runtime overhead beyond the standard library's mutex.

  8. Works with Any Type: Not limited to JSON or maps. Works with any type that supports move semantics.

Real-World Scenarios

Configuration Management:

siddiqsoft::RWLEnvelope<AppConfig> config;

// Multiple threads reading config
auto myDatabaseUrl = config.observe([](const auto& cfg) noexcept {
    // GET - returns a copy of the value.
    return cfg.getDatabaseUrl();
});

// Single thread updating config
config.mutate([](auto& cfg, std::string& newURL) noexcept {
    cfg.setDatabaseUrl(newURL);
    },
    "new_url");

Note we could have used lambda captures but it is preferred to use extra arguments into the callback to resolve r-value and forward parameters into the underlying object.

Cache Implementation:

siddiqsoft::RWLEnvelope<std::unordered_map<std::string, CacheEntry>> cache;

// Fast concurrent reads
auto val = cache.observe([](const auto& c) noexcept {
    return c.at("key");
});

// Exclusive writes
cache.mutate([](auto& c) noexcept {
    c["key"] = computeValue();
});

The motivation is to allow this helper class to handle the locking and contain your in-lock logic restricted to the ideally small/short code within the lambda (or callable).

Shared State in Services:

siddiqsoft::RWLEnvelope<ServiceState> state;

// Multiple reader threads
auto healthCheck = state.observe([](const auto& s) noexcept {
    return s.isHealthy();
});

// Single writer thread
// Passing the myMetrics as argument to the callback
state.mutate([](auto& s, std::map<std::string,std::string>& metrics) noexcept {
    s.updateMetrics( metrics );
    },
    myMetrics);

// And if we're using lambda capture...
state.mutate([&myMetrics](auto& s) noexcept {
    s.updateMetrics( myMetrics );
    });

API Documentation

For comprehensive API documentation, including detailed descriptions of all methods, usage patterns, and examples, see API.md.

Requirements

  • You must be able to use <shared_mutex> and <mutex>.
  • Minimal target is C++20 (requires C++20 concepts support).
  • The build and tests are for MacOS (arm64), Linux (Clang, GCC) and Visual Studio 2019 under arm64 and x64.
  • We use nlohmann::json only in our tests and the library is aware to provide a conversion operator if library is detected.

Usage

  • Use the nuget SiddiqSoft.RWLEnvelope
  • The idea is to not "wrap" the underlying type forcing you to either inherit or re-implement the types but to take advantage of the underlying type's interface whilst ensuring that we have the necessary locks.
  • Two methods:
    • Observer/mutator model with callback and custom return to limit access and to focus the where and how to access the underlying type.
    • Take advantage of init-statement in if-statement to get the contained object within a lock and have the compiler auto-release once we leave scope.
  • A sample implementation (say you want a std::map with reader-writer lock)
    • using RWLMap = siddiqsoft::RWLEnvelope<std::map>;
#include "gtest/gtest.h"
#include "nlohmann/json.hpp"
#include "siddiqsoft/RWLEnvelope.hpp"


TEST(examples, AssignWithCallbacks)
{
	siddiqsoft::RWLEnvelope<nlohmann::json> docl; // we will assign later
	nlohmann::json                          doc2 {{"baa", 0x0baa}, {"fee", 0x0fee}, {"bee", 0x0bee}};

	// Move assign here post init
	docl.reassign(std::move(doc2));
	// Must be empty since we moved it into the envelope
	EXPECT_TRUE(doc2.empty());

	// Check we have pre-change value.. Note that here we return a boolean to avoid data copy
	EXPECT_TRUE(docl.observe([](const auto& doc) noexcept {
		return (doc.value("fee", 0xfa17) == 0x0fee) && (doc.value("baa", 0xfa17) == 0x0baa) && (doc.value("bee", 0xfa17) == 0x0bee);
	}));

	EXPECT_EQ(3, docl.observe([](const auto& doc) noexcept { return doc.size(); }));
}


TEST(examples, AssignWithDirectLocks)
{
	siddiqsoft::RWLEnvelope<nlohmann::json> docl({{"foo", "bar"}, {"few", "lar"}});
	nlohmann::json                          doc2 {{"baa", 0x0baa}, {"fee", 0x0fee}, {"bee", 0x0bee}};

	// Previous document has two items..
	if (auto const& [doc, rl] = docl.readLock(); rl) { EXPECT_EQ(2, doc.size()); }

	// Modify the item (replace the initial with new)
	if (auto [doc, wl] = docl.writeLock(); wl) { doc = std::move(doc2); };

	//doc2 -> Must be empty since we moved it into the envelope
	EXPECT_TRUE(doc2.empty());

	// Check we have post-change value..
	if (const auto& [doc, rl] = docl.readLock(); rl) { EXPECT_EQ(3, doc.size()); }
}

Additional examples.

<details> <summary>

Test Coverage

</summary> The library includes comprehensive test coverage with 38 comprehensive tests across multiple categories:

Basic Functionality Tests

  • Simple Operations: Basic envelope creation and mutation
  • Callback-Based Access: Testing observe() and mutate() methods with various return types
  • Direct Lock Access: Testing readLock() and writeLock() with structured bindings
  • Reassignment: Testing reassign() method for replacing envelope contents
  • Snapshot Operations: Testing snapshot() for independent copies
  • Move Semantics: Testing move constructors and move assignment

Edge Case Tests

  • Default Construction: Envelopes with default-constructed objects
  • Return Value Forwarding: Callbacks returning various types (void, int, string, bool, size_t)
  • Non-JSON Types: Testing with std::vector<int>, std::string, and other types
  • Move Constructor Behavior: Verifying source envelope state after move operations
  • RWA Counter Tracking: Validating the read-write-action counter accuracy
  • Exception Safety: Testing behavior when callbacks throw exceptions
  • Independent Snapshots: Verifying snapshots are truly independent copies
  • Multiple Reassignments: Testing repeated reassignment operations

Concurrency & Stress Tests

Reader-Writer Contention

  • Two-Thread Tests: Concurrent readers and writers with callbacks and direct locks
  • Monotonic Counter Integrity: Verifying counter never goes backwards under concurrent access
  • Snapshot Consistency: Ensuring snapshots return internally consistent state
  • Concurrent Reassign: Testing reassign racing with observe and snapshot operations

High-Contention Scenarios

  • Zero-Sleep Maximum Contention: All threads hammer the lock without delays
  • Mixed API Concurrency: All 5 API methods used concurrently on the same envelope
  • Shared Read Lock Concurrency: Multiple readers accessing simultaneously without blocking
  • Concurrent Observe with Return: Readers returning values under write contention

Data Integrity Verification

  • RWA Counter Accuracy: Verifying mutation counter matches exact mutate() count
  • Paired Field Consistency: Ensuring related fields remain synchronized
  • Version-Data Pairing: Validating version and data fields stay in sync during reassignment

Running Tests

Tests are built using Google Test (gtest) and can be run via the CMake build system:

cmake --preset Apple-Debug
cmake --build --preset Apple-Debug
ctest --preset Apple-Debug

</details>

<small align="right">

© 2021 Abdulkareem Siddiq. All rights reserved.

</small>

Product Compatible and additional computed target framework versions.
native native is compatible. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on SiddiqSoft.RWLEnvelope:

Package Downloads
SiddiqSoft.asynchrony

Add asynchrony to your apps

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.5.2 98 7/5/2026
1.5.1 94 7/4/2026
1.5.0 238 6/27/2026
1.4.5 116 6/25/2026
1.4.4 159 5/22/2026
1.4.3 191 4/29/2026
1.2.0 595 12/7/2024
1.1.1 940 12/2/2021
1.1.0 9,735 7/22/2021
1.1.0-main0001 732 7/22/2021
1.0.0 1,966 7/22/2021
0.10.0-main0001 727 7/22/2021

## Features
- Header-only C++ template library (no compilation needed)
- Simple, intuitive API for thread-safe access patterns
- Automatic lock management with RAII semantics
- Support for both callback-based and direct lock access
- Exception-safe with proper lock release on errors
- Works with any type supporting move semantics
- Zero runtime overhead beyond standard library mutex
- Verified deadlock-free and race-condition-free
- Supports C++20 and later
- Enhanced Visual Studio debugging with natvis visualizers

## What's New
- BREAKING CHANGE:
 - mutate and observe have strict requirements.
 - compiler requirement now C++20