Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.20)

project(msocket LANGUAGES C CXX VERSION 2.0.0)
project(msocket LANGUAGES C CXX VERSION 2.0.1)

set(CMAKE_DISABLE_IN_SOURCE_BUILD ON)
set(CMAKE_DISABLE_SOURCE_CHANGES ON)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ ctest --preset clang-asan
# ThreadSanitizer (TSan)
cmake --preset clang-tsan
cmake --build --preset clang-tsan
ctest --preset clang-tsan
ctest --preset clang-tsan -V

# Static Analysis
cmake --preset clang-tidy
Expand Down
2 changes: 1 addition & 1 deletion docs/Doxyfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Doxyfile for Sphinx + Breathe integration
PROJECT_NAME = "msocket"
PROJECT_NUMBER = "2.0.0"
PROJECT_NUMBER = "2.0.1"
PROJECT_BRIEF = "Event-driven socket library for Linux and Windows"

# Input configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
project = 'msocket'
copyright = '2026, Conny Gustafsson'
author = 'Conny Gustafsson'
release = '2.0.0'
release = '2.0.1'

# -- General configuration ---------------------------------------------------

Expand Down
36 changes: 31 additions & 5 deletions docs/msocket_server.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,41 @@ The ``msocket_server`` module manages listening sockets and handles accepted cli
Architecture & Acceptance Workflow
-----------------------------------

1. Initialize the server using :cpp:func:`msocket_server_create` or :cpp:func:`msocket_server_new`.
The server operates in one of two modes depending on the destructor passed to :cpp:func:`msocket_server_create` or :cpp:func:`msocket_server_new`:

Default Auto-Reap Mode
~~~~~~~~~~~~~~~~~~~~~~

When initialized with ``NULL`` or :cpp:func:`msocket_vdelete` as the destructor, accepted sockets are managed automatically:

1. Initialize the server using :cpp:func:`msocket_server_create` (e.g. ``msocket_server_create(srv, family, NULL)``) or :cpp:func:`msocket_server_new` (e.g. ``msocket_server_new(family, NULL)``).
2. Register an accept handler callback table with :cpp:func:`msocket_server_set_handler`.
Inside the ``stream_accept`` callback (see :doc:`Socket Event Handler <socket_handler>`):

- Configure per-connection event handlers on the child socket using :cpp:func:`msocket_set_handler`.
- Attach the child socket to the server using :cpp:func:`msocket_set_server(child_socket, srv) <msocket_set_server>` for automatic cleanup upon disconnect.
- Attach per-connection event handlers to the child socket using :cpp:func:`msocket_set_handler`.
- Start background I/O on the child connection by calling :cpp:func:`msocket_start_io(child_socket) <msocket_start_io>`.
3. Start the server on the desired TCP/UDP port using :cpp:func:`msocket_server_start` or on a UNIX domain socket using :cpp:func:`msocket_server_unix_start`.
4. When stopping, destroy the server with :cpp:func:`msocket_server_destroy` or :cpp:func:`msocket_server_delete`, which stops all listening threads and cleans up reaped client resources.
- *Note:* Accepted child sockets are automatically cleaned up by the server upon disconnect. When the remote client disconnects, the socket's background thread automatically enqueues the raw socket to the cleanup thread for asynchronous deletion.

3. Start listening with :cpp:func:`msocket_server_start` (TCP/UDP) or :cpp:func:`msocket_server_unix_start` (UNIX domain).
4. When stopping, destroy the server with :cpp:func:`msocket_server_destroy` or :cpp:func:`msocket_server_delete`.

Custom Wrapper Mode
~~~~~~~~~~~~~~~~~~~

When wrapping client sockets inside application-level connection or session structures (as in APX):

1. Initialize the server with a custom destructor function: :cpp:func:`msocket_server_new` (e.g. ``msocket_server_new(family, on_custom_conn_delete)``).
2. Inside ``stream_accept``:

- Allocate your wrapper structure and wrap the accepted ``child_socket``.
- Configure per-connection event handlers on ``child_socket``, passing the wrapper as callback context.
- Start background I/O on ``child_socket`` using :cpp:func:`msocket_start_io`.
- *Note:* Accepted child sockets are not automatically cleaned up by the server in this mode, preventing raw socket pointers from being passed to the custom destructor.

3. Inside the ``stream_disconnected`` handler of your wrapper:

- Enqueue the wrapper structure for deletion by calling :cpp:func:`msocket_server_cleanup_connection` (e.g. ``msocket_server_cleanup_connection(srv, wrapper)``).
- The server cleanup thread invokes your custom destructor, safely deleting both the wrapper and the inner socket.

API Reference
-------------
Expand Down
8 changes: 6 additions & 2 deletions docs/socket_handler.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,12 @@ Invoked on the server when a remote client initiates a new connection.
**Implementation Contract:** Configure the child socket before starting I/O:

1. Attach per-client callbacks and context: ``msocket_set_handler(child, &client_handler, client_ctx);``
2. Bind to server for automatic lifecycle cleanup: ``msocket_set_server(child, srv);``
3. Start background I/O: ``msocket_start_io(child);``
2. Start background I/O: ``msocket_start_io(child);``

*Connection Lifecycle Cleanup:*

* Under **Default Auto-Reap Mode** (server created with ``NULL`` or :cpp:func:`msocket_vdelete`), the server accept task automatically associates the child socket for background cleanup upon disconnect. Manual binding is not required.
* Under **Custom Wrapper Mode** (server created with a custom destructor), wrap the socket in your application connection object, listen for ``stream_disconnected``, and call :cpp:func:`msocket_server_cleanup_connection` (e.g. ``msocket_server_cleanup_connection(srv, wrapper)``).

stream_connected
~~~~~~~~~~~~~~~~
Expand Down
12 changes: 11 additions & 1 deletion examples/echo/echo_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ static void on_client_disconnected(void *arg, void *socket)
{
(void)arg;
(void)socket;
/*
* Note: In default auto-reap mode, the disconnected child socket is automatically
* reaped and deleted by the server's background cleanup thread. Do not call
* msocket_delete here.
*/
printf("[SERVER] Client disconnected\n");
}

Expand Down Expand Up @@ -121,7 +126,12 @@ int main(int argc, char *argv[])

printf("[SERVER] Starting Echo Server on port %u (press Ctrl+C to exit)...\n", port);

msocket_server_t *server = msocket_server_new(MSOCKET_ADDR_INET, msocket_vdelete);
/*
* Initialize server with default auto-reap mode (NULL defaults to msocket_vdelete).
* In this mode, accepted child sockets are automatically bound to the server and
* reaped by the server cleanup thread upon disconnection.
*/
msocket_server_t *server = msocket_server_new(MSOCKET_ADDR_INET, NULL);
if (server == NULL) {
fprintf(stderr, "[SERVER] Failed to allocate msocket_server\n");
return 1;
Expand Down
4 changes: 4 additions & 0 deletions include/msocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ void msocket_set_handler(msocket_t *self, const msocket_handler_t *handler_table
/**
* Associates a parent msocket_server instance with this socket for automatic reaping upon disconnection.
*
* NOTE: For servers created with default destructor (NULL or msocket_vdelete), accepted child
* sockets are associated with the server automatically by the accept task. Manual association
* is only needed when explicitly opting into automatic raw socket reaping under custom setups.
*
* @param self Pointer to msocket_t instance.
* @param server Pointer to parent msocket_server instance, or NULL to detach.
*/
Expand Down
27 changes: 20 additions & 7 deletions include/msocket_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ typedef struct msocket_server_tag {
/**
* Initializes an existing msocket_server instance.
*
* The server supports two connection cleanup modes:
* - Default Auto-Reap Mode (destructor == NULL or msocket_vdelete): Accepted child sockets
* are automatically cleaned up by the server. When disconnected, the socket's background I/O
* thread automatically queues the raw socket for deletion via msocket_vdelete.
* - Custom Wrapper Mode (destructor != msocket_vdelete): Accepted child sockets are NOT
* automatically cleaned up by the server. The application should wrap the child socket in a custom structure
* and invoke msocket_server_cleanup_connection(self, wrapper) from the stream_disconnected callback.
*
* @param self Pointer to msocket_server_t instance.
* @param address_family Address family (MSOCKET_ADDR_INET, MSOCKET_ADDR_INET6, or MSOCKET_ADDR_UNIX).
* @param destructor Destructor function for cleanup items, or NULL to use default msocket_vdelete.
Expand All @@ -65,6 +73,9 @@ void msocket_server_destroy(msocket_server_t *self);
/**
* Dynamically allocates and initializes a new msocket_server instance.
*
* Supports default auto-reap mode (destructor == NULL or msocket_vdelete) or custom
* wrapper mode (custom destructor function).
*
* @param address_family Address family (MSOCKET_ADDR_INET, MSOCKET_ADDR_INET6, or MSOCKET_ADDR_UNIX).
* @param destructor Destructor function for cleanup items, or NULL to use default msocket_vdelete.
* @return Pointer to newly allocated msocket_server_t, or NULL on failure.
Expand All @@ -81,13 +92,13 @@ void msocket_server_delete(msocket_server_t *self);
/**
* Registers the server handler table.
*
* The `handler->tcp_accept` callback is called whenever a new client connection is accepted:
* void on_accept(void *arg, msocket_server_t *srv, msocket_t *child_socket)
* The `handler->stream_accept` callback is called whenever a new client connection is accepted:
* void on_accept(void *arg, msocket_server_t *srv, void *socket)
* Inside that callback, the application should attach per-connection handlers to `child_socket`
* via msocket_set_handler(), and then call msocket_start_io(child_socket).
*
* @param self Pointer to msocket_server_t instance.
* @param handler Pointer to handler table containing tcp_accept callback.
* @param handler Pointer to handler table containing stream_accept callback.
* @param handler_arg User context pointer passed to callback functions.
*/
void msocket_server_set_handler(msocket_server_t *self, const msocket_handler_t *handler, void *handler_arg);
Expand Down Expand Up @@ -118,20 +129,22 @@ void msocket_server_unix_start(msocket_server_t *self, const char *socket_path);
void msocket_server_disable_cleanup(msocket_server_t *self);

/**
* Enqueues a disconnected connection socket for asynchronous deletion by the cleanup thread.
* Enqueues an item directly for asynchronous deletion by the cleanup thread.
*
* @param self Pointer to msocket_server_t instance.
* @param arg Pointer to connection object (passed to destructor).
* @param arg Pointer to object passed to destructor.
*/
void msocket_server_reap_connection(msocket_server_t *self, void *arg);

/**
* Enqueues a closed connection socket or wrapper item for asynchronous deletion by the cleanup thread.
* Safely enqueues a closed connection socket or wrapper item for asynchronous deletion.
*
* Safe to call from within client connection callbacks (e.g. `stream_disconnected`).
* In default auto-reap mode (destructor == msocket_vdelete), this safely detaches the socket
* from the server before enqueuing to prevent double-reaping upon I/O thread termination.
*
* @param self Pointer to msocket_server_t instance.
* @param arg Pointer to connection object (passed to destructor).
* @param arg Pointer to connection object or wrapper (passed to destructor).
*/
void msocket_server_cleanup_connection(msocket_server_t *self, void *arg);

Expand Down
6 changes: 2 additions & 4 deletions source/msocket_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ namespace
auto child_socket = reinterpret_cast<msocket_t*>(socket);
if ((listener != nullptr) && (child_socket != nullptr))
{
msocket_set_server(child_socket, nullptr);
auto accepted = std::make_unique<msocket::TcpSocket>(child_socket, true);
listener->on_connection_accepted(std::move(accepted));
}
Expand Down Expand Up @@ -181,15 +182,12 @@ namespace msocket
{
if (m_socket != nullptr)
{
msocket_close(m_socket);
TcpSocket::set_listener(nullptr);
if (m_owns_socket)
{
msocket_delete(m_socket);
}
else
{
msocket_close(m_socket);
}
m_socket = nullptr;
}
m_listener = nullptr;
Expand Down
37 changes: 29 additions & 8 deletions source/msocket_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -343,14 +343,24 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child)

msocket_os_mutex_lock(self->os);
self->state = MSOCKET_STATE_ACCEPTING;
os_socket_t accept_fd = self->os->tcp_sockfd;
msocket_os_mutex_unlock(self->os);

if (OS_SOCKET_IS_INVALID(accept_fd)) {
if (placement_new) {
msocket_destroy(child);
} else {
msocket_delete(child);
}
return NULL;
}

os_socket_t sockfd;
int one = 1;
msocket_error_t result = MSOCKET_NO_ERROR;

if (self->address_family == MSOCKET_ADDR_UNIX) {
sockfd = accept(self->os->tcp_sockfd, NULL, NULL);
sockfd = accept(accept_fd, NULL, NULL);
if (OS_SOCKET_IS_INVALID(sockfd)) {
result = MSOCKET_SOCKET_ERROR;
} else {
Expand All @@ -362,7 +372,7 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child)
struct sockaddr_in6 cli_addr6;
OS_SOCK_LEN_T cli_len = (OS_SOCK_LEN_T)sizeof(cli_addr6);
memset(&cli_addr6, 0, sizeof(cli_addr6));
sockfd = accept(self->os->tcp_sockfd, (struct sockaddr *)&cli_addr6, &cli_len);
sockfd = accept(accept_fd, (struct sockaddr *)&cli_addr6, &cli_len);
if (OS_SOCKET_IS_INVALID(sockfd)) {
result = MSOCKET_SOCKET_ERROR;
} else if (inet_ntop(AF_INET6, &(cli_addr6.sin6_addr), child->stream_info.addr, MSOCKET_ADDRSTRLEN) == NULL) {
Expand All @@ -377,7 +387,7 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child)
struct sockaddr_in cli_addr;
OS_SOCK_LEN_T cli_len = (OS_SOCK_LEN_T)sizeof(cli_addr);
memset(&cli_addr, 0, sizeof(cli_addr));
sockfd = accept(self->os->tcp_sockfd, (struct sockaddr *)&cli_addr, &cli_len);
sockfd = accept(accept_fd, (struct sockaddr *)&cli_addr, &cli_len);
if (OS_SOCKET_IS_INVALID(sockfd)) {
result = MSOCKET_SOCKET_ERROR;
} else if (inet_ntop(AF_INET, &(cli_addr.sin_addr), child->stream_info.addr, MSOCKET_ADDRSTRLEN) == NULL) {
Expand All @@ -391,7 +401,9 @@ msocket_t *msocket_accept(msocket_t *self, msocket_t *child)
}

msocket_os_mutex_lock(self->os);
self->state = MSOCKET_STATE_LISTENING;
if (self->state == MSOCKET_STATE_ACCEPTING) {
self->state = MSOCKET_STATE_LISTENING;
}
msocket_os_mutex_unlock(self->os);

if (result != MSOCKET_NO_ERROR) {
Expand All @@ -416,14 +428,17 @@ static msocket_error_t msocket_start_io_thread(msocket_t *self)
if (self == NULL || self->os == NULL) {
return MSOCKET_INVALID_ARGUMENT_ERROR;
}
msocket_os_mutex_lock(self->os);
if (!self->os->thread_running) {
self->os->thread_running = true;
self->os->io_thread = msocket_thread_create(io_task, self);
if (self->os->io_thread == NULL) {
self->os->thread_running = false;
msocket_os_mutex_unlock(self->os);
return MSOCKET_MEM_ERROR;
}
}
msocket_os_mutex_unlock(self->os);
return MSOCKET_NO_ERROR;
}

Expand Down Expand Up @@ -586,24 +601,30 @@ void msocket_close(msocket_t *self)
return;
}

msocket_os_mutex_lock(self->os);
/* Prevent joining I/O thread from within itself */
if (self->os->thread_running && msocket_thread_is_current(self->os->io_thread)) {
msocket_os_mutex_unlock(self->os);
return;
}

msocket_os_mutex_lock(self->os);
self->state = MSOCKET_STATE_CLOSING;
if (OS_SOCKET_IS_VALID(self->os->tcp_sockfd)) {
OS_SOCKET_SHUTDOWN(self->os->tcp_sockfd);
}
msocket_os_mutex_unlock(self->os);

msocket_thread_t *io_thread = NULL;
if (self->os->thread_running) {
msocket_thread_join(self->os->io_thread);
msocket_thread_delete(self->os->io_thread);
io_thread = self->os->io_thread;
self->os->io_thread = NULL;
self->os->thread_running = false;
}
msocket_os_mutex_unlock(self->os);

if (io_thread != NULL) {
msocket_thread_join(io_thread);
msocket_thread_delete(io_thread);
}

msocket_os_mutex_lock(self->os);
if (OS_SOCKET_IS_VALID(self->os->tcp_sockfd)) {
Expand Down
2 changes: 1 addition & 1 deletion source/msocket_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ static void accept_task(void *arg)
if (child == NULL) {
break;
}
if (self->destructor != NULL) {
if (self->destructor == msocket_vdelete) {
msocket_set_server(child, self);
}
if (self->handler_table.stream_accept != NULL) {
Expand Down
Loading
Loading