diff options
author | dweller <dweller@cabin.digital> | 2024-03-09 00:55:36 +0200 |
---|---|---|
committer | dweller <dweller@cabin.digital> | 2024-03-09 00:55:36 +0200 |
commit | 86d3f93ee338b28ab7d40aa83c129cf6b97ef4b7 (patch) | |
tree | 507a8d66932e6dea9b121dfcbf980f7925575c9f /c/sources/ezipc.h |
Initial commit, 2 years later
Diffstat (limited to 'c/sources/ezipc.h')
-rw-r--r-- | c/sources/ezipc.h | 543 |
1 files changed, 543 insertions, 0 deletions
diff --git a/c/sources/ezipc.h b/c/sources/ezipc.h new file mode 100644 index 0000000..ff69fe1 --- /dev/null +++ b/c/sources/ezipc.h @@ -0,0 +1,543 @@ +/* + * BSD 3-Clause License (BSD-3-Clause) + * + * Copyright (C) 2022 - dweller <dweller@cabin.digital> + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + + +/* + * EZIPC + * EZ(easy) Inter-Process Communication single-file library + * + * + * At the moment, this is just a wrapper around POSIX named pipes (aka FIFOs) + * that creates duplex communication channel between 2 processes. + * + * + * Brief Description + * + * Just create the connections with ezi_create() on a "server" and ezi_connect() + * on a "client" process. Then use ezi_{send,recv}() functions to push data + * around. There is no functional difference between "server" and "client" + * except that "server" is ultimately responsible for OS resources (the created + * named pipes) clean up. + * When you're done, ezi_destroy() the connection on the "server" and + * ezi_disconnect() it on the "client" side. + * + * The library provides a reliable way to send sized frames between 2 processes. + * The data size is enforced, but other than that there are no assumptions about + * possible higher-level usage of this code. If the reader cannot receive full + * frame at once, it is truncated and the reset is discarded. Send and receive + * failures are considered fatal and restart (disconnected, reconnect) of the + * connection is required. + * + * All functions are blocking on their specific action. Create/Connect wait for + * the other end of the connection, send waits for there to be room in the OS + * buffers, recv waits for something to read from OS buffers. + * + * While more than 2 process are able to read from same connection + * (read: named pipes), THIS IS *HIGHLY* INADVISABLE, as there are no guarantees + * about data interleaving in such case. + * + * + * Single-file Usage + * + * Define "EZIPC_IMPL" before this header inclusion into any *.c file to get + * the implementation code injected into said .c file. Compile and run. + * + * + * Examples + * + * See files server.c, client.c, and common.h for a simple usage example. + * + * + * Symbol List + * + * EZIPC_VERSION + * + * typedef struct ezi_conn_s ezi_conn + * + * ezi_conn* ezi_create(const char* conn_path) + * void ezi_destroy(ezi_conn* conn) + * + * ezi_conn* ezi_connect(const char* conn_path) + * void ezi_disconnect(ezi_conn* conn) + * + * bool ezi_send(ezi_conn* conn, const void* data, size_t size) + * bool ezi_recv(ezi_conn* conn, void* data, size_t* size); + * + * See bellow for specific function descriptions. + * + * + * Compliance + * + * This code adheres to C99 standard, and was tested with following command: + * > $ gcc -std=c99 -Wall -Wextra -pedantic + * + * + * Known Issues + * + * 1. When "server" abruptly disconnects "client" gets SIGPIPE "broken pipe" + * signal, which is annoying because it would be way better to just return + * false from last called function and perror() for information. + * + * Workaround: "client" can insert a signal handler and have a callback, or + * accept quick death as a positive. + * + * + * Changelog + * + * v1.2 - ezi_recv() now fails on receiving a size of 0 bytes + * v1.1 - C++ support (extern and void* casts) + * v1 - release + */ + + +#ifndef EZIPC_H +#define EZIPC_H + + +#define EZIPC_VERSION "v1.2" + + +#include <stddef.h> +#include <stdbool.h> + + +#ifdef __cplusplus +extern "C" { +#endif + + +/* + * Opaque pointer to the structure representing the connection. + */ +typedef struct ezi_conn_s ezi_conn; + + +/* + * Creates the "server" (controlling/initial) end of the connection. This end + * of the connection may create underlying OS resources if they do not exist, + * AND IS responsible for cleaning them. + * resources. + * + * BLOCKS: waits for other end to connect. + * + * Arguments: + * IN `conn_path` - path to store the implementation-related resources, + * doubles as a connection identifier. + * + * Returns the connection object as an opaque pointer. + */ +extern ezi_conn* ezi_create(const char* conn_path); + +/* + * Destroys the connection and frees the underlying OS resources if connection + * was established with `ezi_create()` function. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * + * Returns: nothing. + */ +extern void ezi_destroy(ezi_conn* conn); + +/* + * Creates the "client" end of the connection. This end of the connection may + * create underlying OS resources if they do not exist, but IS NOT responsible + * for them. + * + * BLOCKS: waits for other end to connect. + * + * Arguments: + * IN `conn_path` - path to store the implementation-related resources, + * doubles as a connection identifier. + * + * Returns the connection object as an opaque pointer. + */ + +extern ezi_conn* ezi_connect(const char* conn_path); + +/* + * Destroys the connection and frees the underlying OS resources if connection + * was established with `ezi_create()` function. + * Is an alias of `ezi_destroy`. Exists for symmetry in the API. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * + * Returns: nothing. + */ +#define ezi_disconnect(conn) ezi_destroy(conn) + + +/* + * Sends `size` bytes to the other endpoint. + * + * BLOCKS: waits for room in the underlying buffer. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * IN `data` - buffer of `size` bytes to be sent. + * IN `size` - size of the data to be sent, in bytes. + * + * Returns: true on success, false on failure. If failed, the connection is + * thought to be in undefined state and has to be closed and reopened + * for communication to continue. + */ +extern bool ezi_send(ezi_conn* conn, const void* data, size_t size); + +/* + * Receives up to `size` bytes from the other endpoint. If there is more data + * available that `size`, it is *discarded*! + * + * BLOCKS: waits for data in the underlying buffer. + * + * Arguments: + * IN `conn` - connection object that represents the connection. + * OUT `data` - buffer of `size` bytes to be filled with incoming data; + * on success holds the received data, which may be truncated + * to the input `size`; + * on failure data is undefined. + * INOUT `size` - as an input: specifies the size of `data` buffer in bytes; + * as an output: + * on success holds the size of the received data, which may + * be smaller than the `data` buffer size passed as an + * input; + * on failure is unchanged from input. + * + * Returns: true on success, false on failure. If failed, the connection is + * thought to be in undefined state and has to be closed and reopened + * for communication to continue. + */ +extern bool ezi_recv(ezi_conn* conn, void* data, size_t* size); + +#ifdef __cplusplus +} +#endif // EXTERN C + + +#endif // EZIPC_H + + +// +// ========================================================================= +// +// What follows are implementation details +// +// + +#ifdef EZIPC_IMPL +#ifndef EZIPC_IMPLEMENTED +#define EZIPC_IMPLEMENTED // redefinition guard +#ifdef __linux__ + +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <stdint.h> + +#include <unistd.h> +#include <fcntl.h> +#include <sys/types.h> +#include <sys/stat.h> + + +#ifdef __cplusplus +extern "C" { +#endif + + +struct ezi_conn_s +{ + char* path_c2s; + char* path_s2c; + int c2s, s2c; + + bool server; +}; + + +static ezi_conn* _ezi_internal_create(const char* conn_path, + const mode_t modes[2]) +{ + if(!conn_path) return NULL; + + ezi_conn* conn = (ezi_conn*)calloc(1, sizeof(*conn)); + if(!conn) return NULL; + + conn->c2s = -1; + conn->s2c = -1; + + // Populate path strings + { + const size_t path_len = strlen(conn_path); + + conn->path_c2s = (char*)malloc(path_len + 5); + if(!conn->path_c2s) goto cleanup; + + strcat(conn->path_c2s, conn_path); + strcat(conn->path_c2s, "_c2s"); + + conn->path_s2c = (char*)malloc(path_len + 5); + if(!conn->path_s2c) goto cleanup; + + strcat(conn->path_s2c, conn_path); + strcat(conn->path_s2c, "_s2c"); + } + + + // Create POSIX FIFOs + { + if(mkfifo(conn->path_c2s, 0660) != 0) + { + if(errno != EEXIST) + { + perror("[EZIPC] Could not create client->server pipe"); + goto cleanup; + } + } + + if(mkfifo(conn->path_s2c, 0660) != 0) + { + if(errno != EEXIST) + { + perror("[EZIPC] Could not create server->client pipe"); + goto cleanup; + } + } + } + + // Open the FIFOs + { + conn->c2s = open(conn->path_c2s, modes[0]); + if(conn->c2s < 0) + { + perror("[EZIPC] Could not open client->server pipe"); + goto cleanup; + } + + conn->s2c = open(conn->path_s2c, modes[1]); + if(conn->s2c < 0) + { + perror("[EZIPC] Could not open server->client pipe"); + goto cleanup; + } + } + + return conn; + +cleanup: + + if(conn) + { + ezi_destroy(conn); + + if(conn->path_c2s) free(conn->path_c2s); + if(conn->path_s2c) free(conn->path_s2c); + + free(conn); + } + + return NULL; +} + + +ezi_conn* ezi_create(const char* conn_path) +{ + const mode_t modes[2] = { O_RDONLY, O_WRONLY}; + + ezi_conn* conn = _ezi_internal_create(conn_path, modes); + if(conn) conn->server = true; + + return conn; +} + +ezi_conn* ezi_connect(const char* conn_path) +{ + const mode_t modes[2] = { O_WRONLY, O_RDONLY}; + + ezi_conn* conn = _ezi_internal_create(conn_path, modes); + if(conn) conn->server = false; + + return conn; +} + +void ezi_destroy(ezi_conn* conn) +{ + if(!conn) return; + + // NOTE: even if close() errors out, DO NOT retry (even if EINTR) as per + // man 2 close. Just report the error and clobber the FDs. + + if(conn->c2s >= 0) + { + int rc = close(conn->c2s); + if(rc < 0) perror("[EZIPC] Could not close client->server pipe"); + + conn->c2s = -1; + } + + if(conn->s2c >= 0) + { + int rc = close(conn->s2c); + if(rc < 0) perror("[EZIPC] Could not close server->client pipe"); + + conn->s2c = -1; + } + + // I could check and handle remove error here, but I'd rather tell the user + // and not accidentally delete something else + + if(conn->server) + { + if(conn->path_c2s && (remove(conn->path_c2s) != 0)) + perror("[EZIPC] Could not delete client->server pipe"); + + if(conn->path_s2c && (remove(conn->path_s2c) != 0)) + perror("[EZIPC] Could not delete server->client pipe"); + } +} + +// wrapper over write() to handle interrupted syscall +static bool _ezi_write(int ep, const void* buffer, size_t size) +{ + const uint8_t* bytes = (uint8_t*)buffer; + + size_t written = 0; + while(written < size) + { + size_t got = write(ep, bytes + written, size - written); + if(got <= 0)// FIXME: technically < 0 but I don't want an infinite loop + { + perror("[EZIPC] Could not write to pipe"); + return false; + } + + written += size; + } + + return true; +} + + +bool ezi_send(ezi_conn* conn, const void* data, size_t size) +{ + if(!conn) return false; + if(!data) return false; + if(size == 0) return true; + + const int ep = conn->server ? conn->s2c : conn->c2s; + if(ep >= 0) + { + const uint64_t out_size = (uint64_t)size; + if(!_ezi_write(ep, &out_size, sizeof(out_size))) return false; + if(!_ezi_write(ep, data, size)) return false; + + return true; + } + else return false; + +} + +// wrapper over read() to handle interrupted syscall +static bool _ezi_read(int ep, void* buffer, size_t size) +{ + uint8_t* bytes = (uint8_t*)buffer; + + size_t readback = 0; + while(readback < size) + { + ssize_t got = read(ep, bytes + readback, size - readback); + if(got == 0) + { + // NOTE: I think reading 0 bytes in non-blocking mode is fine, + // but since I don't provide non-blocking mode, this is + // still an error. + return false; + } + else if(got < 0) + { + perror("[EZIPC] Could not read from pipe"); + return false; + } + + readback += got; + } + + return true; +} + + +bool ezi_recv(ezi_conn* conn, void* data, size_t* size) +{ + if(!conn) return false; + if(!data) return false; + if(!size) return false; + if(*size == 0) return true; + + const int ep = conn->server ? conn->c2s : conn->s2c; + if(ep >= 0) + { + // NOTE: since this is IPC, we're on the same machine so no need to + // ensure endianness + uint64_t in_size = 0; + if(!_ezi_read(ep, &in_size, sizeof(in_size))) return false; + + if(in_size == 0) return false; + + const uint64_t read_size = in_size > *size ? *size : in_size; + if(!_ezi_read(ep, data, read_size)) return false; + + // If we have left over, get rid of it to guarantee correct read next + // time + for(uint64_t i = 0; i < (in_size - read_size); i++) + { + uint8_t temp; // I know I could read in bigger chunks, but honestly + // I don't care, and assume this is an edge case + if(!_ezi_read(ep, &temp, 1)) return false; + } + + *size = read_size; + return true; + + } else return false; + +} + +#ifdef __cplusplus +} +#endif // EXTERN C + +#else // __linux__ + #error "EZ IPC library is not supported on your platform. Want to port?" +#endif // platforms +#endif // EZIPC_IMPLEMENTED +#endif // EZIPC_IMPL |