unilink  0.4.3
A simple C++ library for unified async communication
Unilink System Architecture

Comprehensive overview of unilink's architecture and design principles.


Table of Contents

  1. Overview
  2. Layered Architecture
  3. Core Components
  4. Design Patterns
  5. Threading Model
  6. Memory Management
  7. Error Handling
  8. Development & Tooling

Overview

Unilink is designed as a layered, modular communication library with clear separation of concerns. The architecture follows SOLID principles and employs modern C++ design patterns.

Design Goals

  • Simplicity: Easy-to-use Builder API
  • Safety: Memory-safe, thread-safe operations
  • Performance: Asynchronous I/O, efficient resource management
  • Flexibility: Modular design, optional features
  • Reliability: Comprehensive error handling and recovery

Layered Architecture

graph TD
User[User Application Layer]
Builder[Builder API Layer<br/>TcpClientBuilder, TcpServerBuilder, etc.]
Wrapper[Wrapper API Layer<br/>TcpClient, TcpServer, Serial]
Transport[Transport Layer<br/>TcpTransport, SerialTransport]
Common[Common Utilities Layer<br/>Thread Safety, Memory Management, Logging]
Platform[Platform Layer<br/>Boost.Asio]
User --> Builder
Builder --> Wrapper
Wrapper --> Transport
Transport --> Common
Common --> Platform

Layer Responsibilities

1. Builder API Layer

  • Purpose: Provide fluent, chainable interface
  • Components: TcpClientBuilder, TcpServerBuilder, SerialBuilder
  • Responsibilities:
    • Configuration validation
    • Object construction
    • Callback registration

2. Wrapper API Layer

  • Purpose: High-level, easy-to-use interfaces
  • Components: TcpClient, TcpServer, Serial
  • Responsibilities:
    • Connection management
    • Data transmission
    • Lifecycle management
    • Event callbacks

3. Transport Layer

  • Purpose: Low-level communication implementation
  • Components: Protocol-specific transports
  • Responsibilities:
    • Actual I/O operations
    • Protocol handling
    • Connection state management

4. Common Utilities Layer

  • Purpose: Shared functionality
  • Components: Thread safety, memory management, logging
  • Responsibilities:
    • Thread-safe operations
    • Memory tracking
    • Error handling
    • Logging system

Core Components

1. Builder System

namespace unilink::builder {
// Base interface
template<typename T>
class BuilderInterface { ... };
// Concrete builders
class TcpClientBuilder : public BuilderInterface<wrapper::TcpClient>;
class TcpServerBuilder : public BuilderInterface<wrapper::TcpServer>;
class SerialBuilder : public BuilderInterface<wrapper::Serial>;
}

Key Features:

  • Method chaining
  • Type-safe configuration
  • Compile-time validation
  • Auto-initialization support

2. Wrapper System

namespace unilink::wrapper {
// Base interface
class ChannelInterface {
virtual void send(const std::string& data) = 0;
virtual void start() = 0;
virtual void stop() = 0;
};
// Implementations
class TcpClient : public ChannelInterface;
class TcpServer : public ChannelInterface;
class Serial : public ChannelInterface;
}

Key Features:

  • Unified interface
  • Automatic resource management
  • Callback-based events
  • Thread-safe operations

3. Transport System

namespace unilink::transport {
// TCP transport
class TcpClient { ... };
class TcpServer { ... };
class TcpServerSession { ... };
// Serial transport
class Serial { ... };
}

Key Features:

  • Boost.Asio based
  • Asynchronous I/O
  • Retry logic (3s default interval, first retry at 100ms)
  • Backpressure guard: callback at high watermark (default 1 MiB), hard cap triggers socket close + Error state
  • Connection pooling (planned)

4. Common Utilities

namespace unilink::common {
// Thread safety
template<typename T>
class ThreadSafeState;
template<typename T>
class AtomicState;
// Memory management
class MemoryPool;
class MemoryTracker;
class SafeDataBuffer;
// Logging
class Logger;
class LogRotation;
// Error handling
class ErrorHandler;
}

Design Patterns

1. Builder Pattern

Purpose: Simplify object creation with many parameters

// Instead of this:
TcpClient(host, port, retry_interval, callbacks...);
// We use this:
auto client = tcp_client(host, port)
.retry_interval(3000) // Optional, 3000ms is default
.on_data(callback)
.build();
client->start(); // Explicit lifecycle control

Benefits:

  • Readable configuration
  • Optional parameters
  • Compile-time validation
  • Method chaining

2. Dependency Injection

Purpose: Enable testability and flexibility

// Interface-based design
class ISerialPort {
virtual void open() = 0;
virtual void close() = 0;
virtual void async_read(...) = 0;
};
// Production implementation
class RealSerialPort : public ISerialPort { ... };
// Test implementation
class MockSerialPort : public ISerialPort { ... };
// Injection in constructor
Serial::Serial(std::shared_ptr<ISerialPort> port) : port_(port) { }

3. Observer Pattern

Purpose: Event notification system

class TcpClient {
std::function<void()> on_connect_;
std::function<void(const std::string&)> on_data_;
std::function<void()> on_disconnect_;
void notify_connect() {
if (on_connect_) on_connect_();
}
void notify_data(const std::string& data) {
if (on_data_) on_data_(data);
}
};

4. Singleton Pattern

Purpose: Global access to shared resources

class Logger {
public:
static Logger& instance() {
static Logger instance;
return instance;
}
private:
Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};

5. RAII (Resource Acquisition Is Initialization)

Purpose: Automatic resource management

class TcpClient {
~TcpClient() {
// Automatically stop and clean up
stop();
}
std::unique_ptr<IoContext> io_context_; // Auto-deleted
std::shared_ptr<Connection> connection_; // Ref-counted
};

6. Template Method Pattern

Purpose: Define algorithm structure with customizable steps

class ChannelInterface {
void start() {
before_start();
do_start(); // Virtual - customizable
after_start();
}
protected:
virtual void do_start() = 0;
virtual void before_start() {}
virtual void after_start() {}
};

Threading Model

Note: For a detailed execution model, see Runtime Behavior.

Overview

graph TD
User[User Thread Application<br/>- Calls API methods<br/>- Receives callbacks]
Queue[Thread-Safe Queue]
IO[I/O Thread Boost.Asio<br/>- Handles network I/O<br/>- Manages connections<br/>- Executes async operations]
User --> Queue
Queue --> IO

Thread Safety Guarantees

1. API Methods

All public API methods are thread-safe:

// Safe to call from multiple threads
client->send("data1"); // Thread 1
client->send("data2"); // Thread 2
client->stop(); // Thread 3

2. Callbacks

Callbacks are executed in the I/O thread context:

.on_data([](const std::string& data) {
// This runs in I/O thread
// Don't block here!
});

3. Shared State

All shared state is protected:

class TcpClient {
ThreadSafeState<State> state_; // Thread-safe
std::atomic<bool> connected_; // Atomic
std::mutex write_mutex_; // Explicit lock
};

IO Context Management

Shared Context (Default)

// Multiple channels share one I/O thread
auto client1 = tcp_client("server1.com", 8080).build();
auto client2 = tcp_client("server2.com", 8080).build();
// Both use shared IoContextManager

Pros:

  • Efficient resource usage
  • Single I/O thread handles all connections

Cons:

  • All connections share same thread

Independent Context

// Each channel has its own I/O thread
auto client = tcp_client("server.com", 8080)
.build();

Pros:

  • Isolation between connections
  • Useful for testing

Cons:

  • More resource usage
  • More threads

Memory Management

1. Smart Pointers

// Ownership
std::unique_ptr<TcpClient> client_; // Exclusive ownership
std::shared_ptr<Session> session_; // Shared ownership
std::weak_ptr<Connection> conn_; // Non-owning reference

2. Memory Pool

For high-performance scenarios, transports use a bucketed pool and an RAII buffer wrapper.

// Acquire/release with size buckets (1KB/4KB/16KB/64KB)
auto buf = GlobalMemoryPool::instance().acquire(4096);
// ... use buf.get() ...
GlobalMemoryPool::instance().release(std::move(buf), 4096);
// Preferred: RAII wrapper used by transports
common::PooledBuffer pooled(common::MemoryPool::BufferSize::MEDIUM);
if (pooled.valid()) {
common::safe_memory::safe_memcpy(pooled.data(), src, pooled.size());
}

Notes:

  • Pool applies to moderate sizes (<=64KB); larger buffers fall back to regular allocations.
  • PooledBuffer enforces bounds in debug paths; .data() is validated before writes.

3. Memory Tracking

Debug memory leaks:

#ifdef UNILINK_ENABLE_MEMORY_TRACKING
class MemoryTracker {
void track_allocation(void* ptr, size_t size);
void track_deallocation(void* ptr);
void report_leaks();
};
#endif

4. Safe Data Buffer

Bounds-checked buffer:

class SafeDataBuffer {
std::vector<uint8_t> data_;
public:
void append(const std::string& str); // Bounds checked
void append_bytes(const std::vector<uint8_t>& bytes);
std::string to_string() const; // Safe conversion
};

Error Handling

Error Propagation Flow

graph TD
Transport[Transport Layer Error<br/>Boost.Asio error_code]
Handler[Error Handler<br/>- Categorize<br/>- Log<br/>- Notify callbacks]
Callback[User Callback<br/>on_error]
Retry[Retry Logic<br/>if applicable]
Transport --> Handler
Handler --> Callback
Handler --> Retry

Error Categories

enum class ErrorCategory {
NETWORK, // Connection, timeout, etc.
CONFIGURATION, // Invalid config
SYSTEM, // OS errors
MEMORY, // Allocation failures
VALIDATION // Input validation
};
enum class ErrorLevel {
INFO, // Informational
WARNING, // Non-critical
ERROR, // Serious error
CRITICAL // Fatal error
};

Error Recovery Strategies

1. Automatic Retry

class RetryPolicy {
unsigned max_attempts{5};
unsigned interval_ms{3000};
bool exponential_backoff{false};
bool should_retry(unsigned attempt);
unsigned get_delay(unsigned attempt);
};

2. Circuit Breaker (Planned)

class CircuitBreaker {
enum class State { CLOSED, OPEN, HALF_OPEN };
State state_{State::CLOSED};
unsigned failure_threshold_{5};
std::chrono::seconds timeout_{30};
bool allow_request();
void record_success();
void record_failure();
};

Configuration System

Compile-Time Configuration

// CMake option: UNILINK_ENABLE_CONFIG
#ifdef UNILINK_ENABLE_CONFIG
namespace config {
class ConfigManager { ... };
}
#endif
// CMake option: UNILINK_ENABLE_MEMORY_TRACKING
#ifdef UNILINK_ENABLE_MEMORY_TRACKING
class MemoryTracker { ... };
#endif

Runtime Configuration

struct TcpClientConfig {
std::string host;
uint16_t port;
unsigned retry_interval_ms{3000}; // Default is 3 seconds
};
// Load from file
auto config = ConfigManager::load_from_file("config.json");
auto client_config = config->get_tcp_client_config("my_client");

Performance Considerations

1. Asynchronous I/O

  • Non-blocking operations
  • Efficient event loop (Boost.Asio)
  • Minimal context switching

2. Zero-Copy Operations

// Avoid unnecessary copies
void send(std::string&& data); // Move semantics
void send(std::string_view data); // View (no copy)

3. Connection Pooling (Planned)

// Reuse connections
class ConnectionPool {
std::queue<Connection*> available_;
std::vector<std::unique_ptr<Connection>> all_;
Connection* acquire();
void release(Connection* conn);
};

4. Memory Pooling

// Avoid repeated allocations
MemoryPool buffer_pool_(1024, 100);
auto buffer = buffer_pool_.allocate(); // Fast!

Extension Points

1. Custom Transports

class MyCustomTransport : public transport::TransportInterface {
void connect() override { /* ... */ }
void send(const std::string& data) override { /* ... */ }
// ... implement interface
};

2. Custom Builders

class MyCustomBuilder : public BuilderInterface<MyWrapper> {
std::unique_ptr<MyWrapper> build() override {
return std::make_unique<MyWrapper>(/* ... */);
}
};

3. Custom Error Handlers

ErrorHandler::instance().register_callback([](const ErrorInfo& error) {
// Custom error handling logic
send_to_monitoring_system(error);
if (error.level == ErrorLevel::CRITICAL) {
trigger_alert();
}
});

Development & Tooling

1. Docker-based Environment

Unilink provides a Docker Compose-based environment to maintain a consistent development environment across various platforms.

Service Purpose Key Features
dev Development Debug build, interactive shell provided
test Testing Sanitizers (ASan/UBSan) and memory tracking enabled
perf Performance Release build-based performance benchmarks
docs Documentation Doxygen documentation generation and HTTP server (port 8000)
prod Production Optimized production image build

Usage Examples:

docker-compose up test # Run all tests and sanitizers
docker-compose up docs # Generate API docs and start server

2. Documentation Generation

You can generate API documentation directly from the source code using Doxygen.

# Direct generation via CMake
make docs
# Generation via Docker
docker-compose up docs

Testing Architecture

1. Dependency Injection

// Easy to mock
class Serial {
Serial(std::shared_ptr<ISerialPort> port) : port_(port) {}
private:
std::shared_ptr<ISerialPort> port_;
};
// In tests
auto mock_port = std::make_shared<MockSerialPort>();
Serial serial(mock_port);

2. Independent Contexts

// Isolated testing
auto client = tcp_client("127.0.0.1", 8080)
.use_independent_context(true) // Own IO thread
.build();

3. State Verification

// Check internal state
ASSERT_EQ(client->get_state(), State::CONNECTED);
ASSERT_TRUE(server->is_listening());

Summary

Unilink's architecture emphasizes:

Modularity - Clear separation of concerns
Safety - Memory-safe, thread-safe operations
Performance - Asynchronous I/O, efficient resource management
Testability - Dependency injection, mockable interfaces
Extensibility - Easy to add new transports and features
Maintainability - Clean code, well-documented


See Also:

  • Design Patterns
  • Threading Model
  • ../guides/performance_tuning.md "Performance Guide"