summaryrefslogtreecommitdiff
path: root/c
diff options
context:
space:
mode:
Diffstat (limited to 'c')
-rwxr-xr-xc/build82
-rw-r--r--c/sources/client.c96
-rw-r--r--c/sources/common.h27
-rw-r--r--c/sources/ezipc.h543
-rw-r--r--c/sources/server.c66
5 files changed, 814 insertions, 0 deletions
diff --git a/c/build b/c/build
new file mode 100755
index 0000000..f452e5a
--- /dev/null
+++ b/c/build
@@ -0,0 +1,82 @@
+#!/usr/bin/env sh
+set -e
+#set -x #trace
+
+CC=gcc
+CFLAGS=" \
+ -pipe \
+ -Wall -Wextra -Wshadow -Wpedantic -Wformat=2 \
+ -std=c99"
+
+
+CFASTF="-O2 -flto"
+CDBGF="-g3 -Og -D _DEBUG"
+
+
+# avoid "bin" just in case something goes wrong and we wipe /bin
+# #paranoia
+LIBDIR="libraries"
+SRCDIR="sources"
+BUILDDIR="binaries"
+
+main()
+{
+ case "$1" in
+ "") build_debug ;;
+ "debug") build_debug ;;
+ "release") build_release ;;
+ "clean") clean ;;
+ "help") help ;;
+ *)
+ echo "Error: Unknown argument '$1'"
+ help
+ ;;
+ esac
+}
+
+help()
+{
+ echo "Build script for Linux OSes"
+ echo "Usage: $0 [debug|release|clean|help]"
+ echo " debug - build debug mode (default)"
+ echo " release - build in release mode, aka optimized"
+ echo " clean - delete the build artifacts"
+ echo " help - print this message"
+ exit
+}
+
+build_unity()
+{
+ "$CC" -D _UNITY_BUILD $CFLAGS -fwhole-program -I $(realpath "$SRCDIR") \
+ -I $(realpath "$LIBDIR") $1 "$SRCDIR"/server.c \
+ -o "$BUILDDIR"/server
+
+ "$CC" -D _UNITY_BUILD $CFLAGS -fwhole-program -I $(realpath "$SRCDIR") \
+ -I $(realpath "$LIBDIR") $1 "$SRCDIR"/client.c \
+ -o "$BUILDDIR"/client
+}
+
+build()
+{
+ mkdir -p "$BUILDDIR"
+
+ build_unity "$1"
+}
+
+build_debug()
+{
+ build "$CDBGF"
+}
+
+build_release()
+{
+ build "$CFASTF"
+}
+
+clean()
+{
+ rm -rf $(realpath ${BUILDDIR:?})
+}
+
+
+main "$@"
diff --git a/c/sources/client.c b/c/sources/client.c
new file mode 100644
index 0000000..672e3e8
--- /dev/null
+++ b/c/sources/client.c
@@ -0,0 +1,96 @@
+#define EZIPC_IMPL
+#include "ezipc.h"
+
+#include "common.h"
+
+
+int main(void)
+{
+ printf("EZIPC C Client\n");
+
+
+ printf("Connecting...");
+ fflush(stdout);
+
+ ezi_conn* conn = ezi_connect(EZIPC_TEST_PATH);
+ assert(conn);
+
+ printf(" DONE!\n");
+
+ msg msg_txt = { .type = MSG_TEXT };
+ msg msg_exit = { .type = MSG_EXIT };
+
+ bool running = true;
+ while(running)
+ {
+ char in[1024] = {0};
+ printf("send> ");
+ scanf("%[^\n]%*c", in);
+
+ if(strcmp(in, "exit") != 0)
+ {
+ strcpy((char*)msg_txt.data, in);
+ if(!ezi_send(conn, &msg_txt, sizeof(msg_txt)))
+ {
+ printf("Send error, resetting...\n");
+
+ ezi_disconnect(conn);
+ conn = ezi_connect(EZIPC_TEST_PATH);
+ assert(conn);
+
+ continue;
+ }
+ }
+ else
+ {
+ if(!ezi_send(conn, &msg_exit, sizeof(msg_exit)))
+ {
+ ezi_disconnect(conn);
+ exit(1);
+ }
+
+ break;
+ }
+
+ msg rmsg = {0};
+ size_t rsz = sizeof(rmsg);
+ if(!ezi_recv(conn, &rmsg, &rsz))
+ {
+ printf("Recv error, resetting...\n");
+
+ ezi_disconnect(conn);
+ conn = ezi_connect(EZIPC_TEST_PATH);
+ assert(conn);
+
+ continue;
+ }
+
+ switch(rmsg.type)
+ {
+ case MSG_OK:
+ {
+ printf("acknowledged!\n");
+
+ } break;
+
+ case MSG_EXIT:
+ {
+ running = false;
+ printf("told to exit...\n");
+
+ } break;
+
+ case MSG_TEXT:
+ {
+ printf("received: '%s'\n", (char*)rmsg.data);
+
+ } break;
+
+ default: exit(1);
+ }
+ }
+
+ ezi_disconnect(conn);
+ return 0;
+}
+
diff --git a/c/sources/common.h b/c/sources/common.h
new file mode 100644
index 0000000..bc3ae30
--- /dev/null
+++ b/c/sources/common.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <assert.h>
+#include <string.h>
+
+
+#define EZIPC_TEST_PATH "/tmp/ezi_conn_test"
+
+
+typedef enum msg_type_e
+{
+ MSG_TEXT,
+ MSG_OK,
+ MSG_EXIT,
+
+} msg_type;
+
+
+typedef struct msg_s
+{
+ msg_type type;
+ uint8_t data[128];
+
+} msg;
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
diff --git a/c/sources/server.c b/c/sources/server.c
new file mode 100644
index 0000000..57c2492
--- /dev/null
+++ b/c/sources/server.c
@@ -0,0 +1,66 @@
+#include "ezipc.h"
+#include "common.h"
+
+
+int main(void)
+{
+ ezi_conn* conn = ezi_create(EZIPC_TEST_PATH);
+ assert(conn);
+
+ msg ok;
+ ok.type = MSG_OK;
+ strcpy((char*)ok.data, "ok");
+
+ msg quit;
+ quit.type = MSG_EXIT;
+ strcpy((char*)quit.data, "exit");
+
+ int counter = 0;
+ bool running = true;
+ while(running)
+ {
+ msg rmsg;
+ size_t rsz = sizeof(rmsg) - 100; // truncate test
+ if(ezi_recv(conn, &rmsg, &rsz))
+ {
+ switch(rmsg.type)
+ {
+ case MSG_TEXT:
+ {
+ printf("got: '%s'\n", (char*)rmsg.data);
+
+ counter++;
+ if(counter >= 10)
+ {
+ if(!ezi_send(conn, &quit, sizeof(quit)))
+ printf("Could not send msg to client\n");
+
+ counter = 0;
+ }
+ else
+ {
+ if(!ezi_send(conn, &ok, sizeof(ok)))
+ printf("Could not send msg to client\n");
+ }
+
+ } break;
+
+ default: break;
+ }
+ }
+ else
+ {
+ printf("Recv error, resetting...\n");
+
+ ezi_destroy(conn);
+ conn = ezi_create(EZIPC_TEST_PATH);
+ assert(conn);
+ }
+ }
+
+ ezi_destroy(conn);
+ return 0;
+}
+
+#define EZIPC_IMPL
+#include "ezipc.h"