/* * BSD 3-Clause License (BSD-3-Clause) * * Copyright (C) 2022 - dweller * * 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. */ #ifndef EZIPC_H #define EZIPC_H #define EZIPC_VERSION "v1.1" #include #include #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 #include #include #include #include #include #include #include #include #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; 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; 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