8.2.1.1. Minimal socket implementation

8.2.1.1.1. Overview

This tutorial demonstrates how to implement a minimal UDP network compatibility layer for Anjay Lite using POSIX sockets. Although Anjay Lite already provides a built-in implementation for POSIX environments, this example helps you understand how to create your own custom network layer.

Note

Code related to this tutorial can be found under examples/custom-network/minimal in the Anjay Lite source directory and is based on examples/tutorial/BC-MandatoryObjects example.

8.2.1.1.2. Update the build configuration

This example uses CMake as a build system and demonstrates how to provide a basic UDP network layer implementation.

To disable the default POSIX socket implementation and enable the custom UDP layer, apply the following changes in CMakeLists.txt:

cmake_minimum_required(VERSION 3.16.0)

project(anjay_lite_minimal_network_api C)

set(CMAKE_C_STANDARD 99)

set(ANJ_WITH_SOCKET_POSIX_COMPAT OFF)
set(ANJ_NET_WITH_UDP ON)
set(ANJ_NET_WITH_TCP OFF)

if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
    set(anjay_lite_DIR "../../../cmake")
    find_package(anjay_lite REQUIRED)
endif()

add_executable(anjay_lite_minimal_network_api src/main.c src/net.c)

The examples/custom-network/minimal/src/net.c file contains the custom network compatibility layer implementation.

8.2.1.1.3. Limitations

For clarity, this example includes only essential functionality and has the following limitations:

  • Supports only UDP protocol.

  • Supports only IPv4 (no IPv6 connections).

  • Does not preserve the local port between multiple connections to the same server.

  • Does not validate input parameters.

  • Performs minimal error handling. The return values of several POSIX functions are not checked, including:

    • fcntl

    • close

    • shutdown

  • Does not support socket configuration, such as selecting the address family.

  • Uses a fixed inner MTU value, which may not reflect actual network conditions.

Despite these limitations, this implementation is sufficient to connect to a LwM2M server using the networking compatibility layer.

8.2.1.1.4. Create socket context

The socket context is represented by a custom structure: net_ctx_posix_impl_t. This structure stores:

  • the socket file descriptor (sockfd)

  • the current socket state (state)

You can modify this structure to fit your needs. The example below shows a basic implementation:

typedef struct net_ctx_posix_impl {
    sockfd_t sockfd;
    anj_net_socket_state_t state;
} net_ctx_posix_impl_t;

int anj_udp_create_ctx(anj_net_ctx_t **ctx_, const anj_net_config_t *config) {
    (void) config;

    net_ctx_posix_impl_t *ctx =
            (net_ctx_posix_impl_t *) malloc(sizeof(net_ctx_posix_impl_t));
    if (!ctx) {
        return NET_GENERAL_ERROR;
    }
    ctx->sockfd = INVALID_SOCKET;
    ctx->state = ANJ_NET_SOCKET_STATE_CLOSED;

    *ctx_ = (anj_net_ctx_t *) ctx;
    return ANJ_NET_OK;
}

The anj_udp_create_ctx function initializes the network context by allocating memory for the net_ctx_posix_impl_t structure and initialize its values.

Note

If dynamic memory allocation is not allowed in the project, this function can assign the ctx_ pointer to a static global structure instead, omitting the need to use the malloc function.

Note

The only positive values that network API functions may return are error codes with the ANJ_NET_E prefix defined in the include_public/anj/compat/net/anj_net_api.h file (e.g. ANJ_NET_EAGAIN); all other positive values are forbidden - they are reserved for client-side logic for potential new error codes. Please refer to anj_net_api.h for a full list of defined error codes and description when specific network API functions can return them.

NET_GENERAL_ERROR is defined as -1, but any negative value may be returned to indicate an error, Anjay Lite will treat them in the same way.

8.2.1.1.5. Connect

First, define a helper function to set sockets to non-blocking mode.d This prevents Anjay Lite from halting while waiting for incoming data.

static void set_socket_non_blocking(sockfd_t sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags >= 0) {
        fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
    }
}

Now implement the anj_udp_connect function:

int anj_udp_connect(anj_net_ctx_t *ctx_,
                    const char *hostname,
                    const char *port_str) {
    net_ctx_posix_impl_t *ctx = (net_ctx_posix_impl_t *) ctx_;

    struct addrinfo *serverinfo = NULL;
    struct addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_DGRAM;

    if (getaddrinfo(hostname, port_str, &hints, &serverinfo) || !serverinfo) {
        if (serverinfo) {
            freeaddrinfo(serverinfo);
        }
        return NET_GENERAL_ERROR;
    }

    ctx->sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (ctx->sockfd < 0) {
        freeaddrinfo(serverinfo);
        return NET_GENERAL_ERROR;
    }

    if (connect(ctx->sockfd, serverinfo->ai_addr, serverinfo->ai_addrlen)) {
        freeaddrinfo(serverinfo);
        return NET_GENERAL_ERROR;
    }
    set_socket_non_blocking(ctx->sockfd);
    ctx->state = ANJ_NET_SOCKET_STATE_CONNECTED;

    freeaddrinfo(serverinfo);
    return ANJ_NET_OK;
}

8.2.1.1.5.1. How it works

The anj_udp_connect function performs the following steps:

Resolve server address

It uses getaddrinfo() to convert the provided hostname and port into a list of address structures suitable for an IPv4 UDP connection.

  • hints.ai_family is set to AF_INET (IPv4).

  • hints.ai_socktype is set to SOCK_DGRAM (UDP).

Attention

The getaddrinfo function may block, which can halt the execution of Anjay Lite during the call. If non-blocking behavior is required, use an asynchronous variant if available. If the connect operation is pending in a non-blocking scenario, return ANJ_NET_EINPROGRESS to inform Anjay Lite that it needs to be called again to finish establishing the connection. Note that returning ANJ_NET_EINPROGRESS again and again will block the library from performing any other operation and falling back with error handling procedure.

Create a socket and connect it to the server

It creates a new IPv4 UDP socket with socket(AF_INET, SOCK_DGRAM, 0). Then, it connects the socket to the server address obtained earlier.

If the connection succeeds, the socket is ready for communication with the target host.

Set socket to non-blocking mode

It ensures the socket is configured as non-blocking to prevent delays during future send and recv operations.

Update socket state

It updates the socket’s state to ANJ_NET_SOCKET_STATE_CONNECTED.

Release address information

It frees the memory allocated by getaddrinfo().

Note

Always keep the socket state correctly updated. Anjay Lite relies on the socket state to determine the current connection status.

8.2.1.1.6. Send

Before implementing the send functionality, create a helper function to check if an error indicates a blocking condition:

static bool would_block(int errno_val) {
    switch (errno_val) {
#ifdef EAGAIN
    case EAGAIN:
        return true;
#endif
#if defined(EWOULDBLOCK) && (EWOULDBLOCK != EAGAIN)
    case EWOULDBLOCK:
        return true;
#endif
#ifdef EINPROGRESS
    case EINPROGRESS:
        return true;
#endif
#ifdef EBUSY
    case EBUSY:
        return true;
#endif
    default:
        return false;
    }
}

The would_block function checks the errno value set by the POSIX socket system calls. It returns true if the error code indicates that the operation would have blocked. Otherwise, it returns false.

Now implement anj_udp_send:

int anj_udp_send(anj_net_ctx_t *ctx_,
                 size_t *bytes_sent,
                 const uint8_t *buf,
                 size_t length) {
    net_ctx_posix_impl_t *ctx = (net_ctx_posix_impl_t *) ctx_;
    errno = 0;
    ssize_t result = send(ctx->sockfd, buf, length, 0);
    if (result < 0) {
        return would_block(errno) ? ANJ_NET_EINPROGRESS : NET_GENERAL_ERROR;
    }
    *bytes_sent = (size_t) result;
    if (*bytes_sent < length) {
        /* Partial sent not allowed in case of UDP */
        return NET_GENERAL_ERROR;
    }
    return ANJ_NET_OK;
}

How it works

The anj_udp_send function acts as a simple wrapper around the standard POSIX send call:

  • Sends the data from the buffer to the connected socket.

  • If an error occurs it checks whether the error indicates a non-blocking situation.

  • Returns an error if only part of the data was sent.

  • On success, reports the number of bytes sent via the bytes_sent output parameter.

8.2.1.1.7. Receive

Receiving follows the same pattern as sending:

int anj_udp_recv(anj_net_ctx_t *ctx_,
                 size_t *bytes_received,
                 uint8_t *buf,
                 size_t length) {
    net_ctx_posix_impl_t *ctx = (net_ctx_posix_impl_t *) ctx_;
    errno = 0;
    ssize_t result = recv(ctx->sockfd, buf, length, 0);
    if (result < 0) {
        // in anj_net api, recv differentiates between EAGAIN and EINPROGRESS
        if (errno == EAGAIN) {
            return ANJ_NET_EAGAIN;
        }
        return would_block(errno) ? ANJ_NET_EINPROGRESS : NET_GENERAL_ERROR;
    }
    *bytes_received = (size_t) result;
    if (*bytes_received == length) {
        /**
         * Buffer entirely filled - data possibly truncated. This will
         * incorrectly reject packets that have exactly buffer_length
         * bytes, but we have no means of distinguishing the edge case
         * without recvmsg.
         * This does only apply to datagram sockets (in our case: UDP).
         */
        return ANJ_NET_EMSGSIZE;
    }
    return ANJ_NET_OK;
}

Note

The anj_upd_recv function differentiates between EAGAIN and EINPROGRESS. That is the only function in the Anjay Lite network API that does so. EAGAIN indicates that no data is currently available for reading, EINPROGRESS indicates any state where the operation would block while receiving data.

Note

If the buffer is too small to hold the incoming packet, or matches it exactly anj_udp_recv returns ANJ_NET_EMSGSIZE. This informs Anjay Lite to drop the packet gracefully. Any other error is treated as fatal and triggers a connection reset.

8.2.1.1.8. Shutdown

anj_udp_shutdown function is straightforward but requires updating the socket context’s state to ANJ_NET_SOCKET_STATE_SHUTDOWN upon completion.

int anj_udp_shutdown(anj_net_ctx_t *ctx_) {
    net_ctx_posix_impl_t *ctx = (net_ctx_posix_impl_t *) ctx_;

    shutdown(ctx->sockfd, SHUT_RDWR);

    ctx->state = ANJ_NET_SOCKET_STATE_SHUTDOWN;
    return ANJ_NET_OK;
}

8.2.1.1.9. Close

anj_udp_close closes the underlying socket and updates the socket state to ANJ_NET_SOCKET_STATE_CLOSED indicating that it is no longer active.

int anj_udp_close(anj_net_ctx_t *ctx_) {
    net_ctx_posix_impl_t *ctx = (net_ctx_posix_impl_t *) ctx_;

    close(ctx->sockfd);

    ctx->sockfd = INVALID_SOCKET;
    ctx->state = ANJ_NET_SOCKET_STATE_CLOSED;
    return ANJ_NET_OK;
}

Note

The context object itself is not cleared here to preserve data for possible reuse.

8.2.1.1.10. Context cleanup

The cleanup function releases all resources associated with the socket context. First, it closes the socket if it is still open, then it frees the dynamically allocated memory that stored the socket context’s state.

int anj_udp_cleanup_ctx(anj_net_ctx_t **ctx_) {
    net_ctx_posix_impl_t *ctx = (net_ctx_posix_impl_t *) *ctx_;
    *ctx_ = NULL;

    close(ctx->sockfd);
    free(ctx);

    return ANJ_NET_OK;
}

8.2.1.1.11. Get inner MTU

anj_udp_get_inner_mtu returns the assumed maximum transmission unit (MTU) for IPv4 UDP datagrams, excluding protocol headers:

int anj_udp_get_inner_mtu(anj_net_ctx_t *ctx, int32_t *out_value) {
    (void) ctx;
    *out_value = 548; /* 576 (IPv4 MTU) - 28 bytes of headers */
    return ANJ_NET_OK;
}

Note

This is a static implementation. In a real-world project, retrieve the actual MTU dynamically using getsockopt().

8.2.1.1.12. Get state

The anj_udp_get_state function lets Anjay Lite retrieve the current connection status of the context.

int anj_udp_get_state(anj_net_ctx_t *ctx_, anj_net_socket_state_t *out_value) {
    net_ctx_posix_impl_t *ctx = (net_ctx_posix_impl_t *) ctx_;
    *(anj_net_socket_state_t *) out_value = ctx->state;
    return ANJ_NET_OK;
}

8.2.1.1.13. Queue Mode RX off

anj_udp_queue_mode_rx_off function hints the transport that the client will not need to receive application data until it initiates the next outgoing exchange. Behavior really depends on the targeted hardware platform. In this minimal POSIX implementation, this functionality is not applicable and simply returns ANJ_NET_OK.

int anj_udp_queue_mode_rx_off(anj_net_ctx_t *ctx_) {
    (void) ctx_;
    return ANJ_NET_OK;
}

8.2.1.1.14. Summary

This example provides a minimal custom UDP socket layer for Anjay Lite. It covers connection management, send/receive operations, and cleanup. While not suitable for production use, it serves as a clear reference for integrating Anjay Lite with a platform-specific network stack.