CPort (Work In Progress)

Frank Mitchell

Posted: 2023-03-16
Word Count: 1901
Tags: c-programming programming vaporware

Table of Contents

Purpose

CPort stands for both C Portability and Character Ports. As the overloaded name suggests, its goal is to provide an API to read and write streams of Unicode code points on any platform that supports C, even embedded and non-standard platforms that cannot use <stdio.h>.

Concepts

Ports

A port provides a stream of characters. An input port provides an incoming stream for an application to read; an output port provides an outgoing stream to which an application may write.

From the application’s perspective, they use ports something like this1:

C_Port_Env *portenv;

const char* uri = "file:///home/fmitchell/test.json"
C_Port* inport;

CPORT_TRY_WITH(inport = C_Port_open_input(portenv, uri))

    wchar_t cp;

    do {
        cp = C_Port_read_char(inport);
        CPORT_CHECK(inport);

        /* do something with code point `cp` */

    } while (cp >= 0);

CPORT_CATCH(error)

    /** Handle error somewhow */

CPORT_END_TRY

/* has called C_Port_close(inport) */

As an abstraction layer over innumerable sources and sinks of text, ports provide no advanced functions to rewind or fast-forward along a file, interrogate the size or source of the stream, and so on.

Port Environment

A port environment defines an unbounded set of ports with common characteristics:

Environments implement interactions and restrictions on those actions through handlers and environment variables accessible to those handlers.

Converter

To convert read octets (8-bit bytes) to UTF-8 or UTF-32, and to convert UTF-32 or UTF-8 back to arbitrary octets, C-Port will use a custom library called C-Converter to do the conversions.

Like the API below this avoids a direct dependency on libraries like iconv which may not exist – or be necessary – on all platforms.

Readers and Writers

At the lowest level are Readers and Writers. Each Reader or Writer consists of a Reader or Writer function and a void* pointer containing the data necessary to read or write.2

typedef unsigned char octet_t;

/**
 * Each invocation reads bytes from `data`, 
 * set `(*sptr)` to the number of bytes read,
 * and returns a pointer containing the read bytes.
 *
 * Only one thread should attempt to read from one *rdata at a time.
 *
 * If the result == NULL the reader has reached the end of available input.
 *
 * The result will become invalid after the next invocation.
 */
typedef const octet_t* (*C_Port_Reader)(C_Port* port, 
                                        void* rdata,
                                        size_t *sptr);

/**
 * Each invocation attempts to write `size` bytes from `buf` to `wdata`.
 *
 * Only one thread should attempt to read from one *wdata at a time.
 *
 * The result is the number of bytes written, or negative if the resource
 * cannot be written to for some reason.
 */
typedef size_t (*C_Port_Writer)(C_Port* port, 
                                void* wdata, 
                                size_t size, 
                                const octet_t* buf);

API

The full CPort API will look something like this.

Types

/* defined in C99 */
#include <stdbool.h> /* defines bool */
#include <stddef.h>  /* defines size_t */
#include <stdint.h>  /* defines uint8_t */
#include <wchar.h>   /* defines wchar_t */

typedef uint8_t octet_t;
typedef uint8_t utf8_t;

typedef struct _C_Port_Env  C_Port_Env;
typedef struct _C_Port      C_Port;

Result

typedef enum _C_Port_Status {
    CPORT_OK = 0,

    CPORT_OPEN_ERROR     = 0x100,
    CPORT_NAME_NOT_FOUND = 0x101,
    /* to be continued ... */

    CPORT_READ_ERROR     = 0x200,
    /* to be continued ... */

    CPORT_WRITE_ERROR    = 0x300,
    /* to be continued ... */

    CPORT_CONV_ERROR     = 0x500,
    /* to be continued ... */

    CPORT_CLOSE_ERROR    = 0x800,
    CPORT_END_OF_STREAM  = 0x801,
    /* to be continued ... */

    /* Uncategorized Error */
    CPORT_UNKNOWN_ERROR = 0x7FFF
} C_Port_Status;

/* TODO: cross-reference with <errno.h> */

Callbacks

typedef enum _C_Port_Method {
    METHOD_NONE    = 0x0000,    /* used only in C_Port_Config_Entry array */
    METHOD_CONNECT = 0x0001,    /* full duplex read/write connection */
    METHOD_DELETE  = 0x0002,    /* delete resource */
    METHOD_GET     = 0x0004,    /* read resource */
    METHOD_HEADERS = 0x0008,    /* [reserved] */
    METHOD_OPTIONS = 0x0010,    /* [reserved] */
    METHOD_PATCH   = 0x0020,    /* [reserved] */
    METHOD_POST    = 0x0040,    /* update or append to resource */
    METHOD_PUT     = 0x0080,    /* replace or (over)write resource */
    METHOD_TRACE   = 0x0100,    /* [reserved] */
    METHOD_UNKNOWN = 0xFFFF     /* some other method */
} C_Port_Method;

/*
 * Callbacks to open (create) the implementation of a Port.
 */
typedef C_Port_Status (*C_Port_Opener)(C_Port_Env* env,
                                        C_Port* port,
                                        const char* name, 
                                        C_Port_Method method,
                                        void* (*data));

/*
 * Callback to close (delete) the implementation of a Port.
 */
typedef C_Port_Status (*C_Port_Closer)(C_Port_Env* env, void* data);

/*
 * Callback to read bytes and return a pointer to the bytes read.
 */
typedef const octet_t* (*C_Port_Reader)(C_Port* port, 
                                        void* data,
                                        size_t *sptr);

/*
 * Callback to write `size` bytes from `buf`.
 */
typedef size_t (*C_Port_Writer)(C_Port* port,
                                void* data, 
                                size_t size, 
                                const octet_t* buf);

/*
 * Callback to delete a resource by name, and return whether it succeeded.
 */
typedef C_Port_Status (*C_Port_Deleter)(C_Port_Env* env, const char* name);

Env

typedef struct _C_Port_Config_Entry {
    C_Port_Method method;
    const char* prefix;
    union {
        struct {
            C_Port_Reader read;
            C_Port_Writer write;
        } connect;
        C_Port_Reader get;
        C_Port_Writer put;
        C_Port_Writer post;
        C_Port_Deleter delete;
    } callback;
    C_Port_Opener callback_open;
    C_Port_Closer callback_close;
} C_Port_Config_Entry;

/**
 * Create a reentrant CPort Env (environment) to handle ports.
 * If `conv` is not NULL, `conc` should be the number of C_Port_Config_Entry
 * structs in the list.  For safety the last one should have method
 * METHOD_NONE and NULLs for all other values.
 */
C_Port_Env* C_Port_open(int conc, C_Port_Config_Entry* conv);

/**
 * Get a previously stored pointer for `key`, 
 * or NULL if no such key.
 */
const void* C_Port_Env_get(C_Port_Env* env, const char* key);

/**
 * Store a pointer value for `key`.
 * Used mainly by C_Port_Opener to inform or constrain what files, URLs, etc.
 * it may open in the current sandbox.
 */
void C_Port_Env_set(C_Port_Env* env, const char* value, const void* value);

/**
 * Delete the Env object.
 * This immediately closes and cleans up all open Ports.
 */
C_Port_Status C_Port_close(C_Port_Env* env);

/*
 * Set callbacks to handle reading *and* writing from a port.
 */
void C_Port_set_port_handler(C_Port_Env* env,
                            C_Port_Method method,
                            const char* prefix,
                            C_Port_Reader rfcn,
                            C_Port_Writer wfcn,
                            C_Port_Deleter dfcn,
                            C_Port_Opener ofcn,
                            C_Port_Closer cfcn);

Error Handling

typedef void (*C_Port_Error_Handler)(C_Port* port, C_Port_Status code);

C_Port_Status C_Port_status(C_Port* port);
void          C_Port_set_status(C_Port* port, C_Port_Status);
void          C_Port_clear_status(C_Port* port);

C_Port_Error_Handler C_Port_Env_handler(C_Port_Env* env;

void C_Port_Env_set_handler(C_Port_Env* env, C_Port_Env_Error_Handler);

Ports

Opening Ports

In the functions below:

C_Port* C_Port_open_input(C_Port_Env* env, const char* path);

C_Port* C_Port_open_output(C_Port_Env* env,
                                    const char* path,
                                    bool append);

C_Port* C_Port_open_stderr(C_Port_Env* env);

C_Port* C_Port_open_stdin(C_Port_Env* env);

C_Port* C_Port_open_stdout(C_Port_Env* env);

C_Port* C_Port_open_uri(C_Port_Env* env, 
                                C_Port_Method method,
                                const char* path);

C_Port_open_uri() can open non-HTTP URIs or ordinary file paths. The method translates as follows:

HTTP Method Files FTP Other Network Protocols
CONNECT fopen(f, "a+") (command line) read/write
DELETE unlink(f) DELETE
GET fopen(f, "r") GET read only
PUT fopen(f, "w") PUT write only
POST fopen(f, "a") write only

Configuring Ports

The set_* methods return boolean values to indicate whether the command succeeded. Generally applications should configure a port before reading or writing data.

const char* C_Port_attribute(C_Port* port, const char* name);
const char* C_Port_encoding(C_Port* port);
bool        C_Port_is_binary(C_Port* port);
const char* C_Port_mime_type(C_Port* port);

bool        C_Port_set_attribute(C_Port* port, 
                                        const char* name,
                                        const char* value);
bool        C_Port_set_binary(C_Port* port, bool bool);
bool        C_Port_set_encoding(C_Port* port, const char* encoding);
bool        C_Port_set_mime_type(C_Port* port, const char* type);

Reading From Input Ports

bool    C_Port_is_end_of_stream(C_Port* port);
bool    C_Port_is_input(C_Port* port);
bool    C_Port_is_ready(C_Port* port);
wchar_t C_Port_peek_char(C_Port* port);
wchar_t C_Port_read_char(C_Port* port);
void    C_Port_read_raw(C_Port* port, 
                                size_t bufsiz,
                                size_t *szptr,
                                octet_t* buffer);
void    C_Port_read_utf8(C_Port* port, 
                                size_t bufsiz,
                                size_t *szptr,
                                utf8_t* buffer);
void    C_Port_skip_char(C_Port* port);

Writing To Output Ports

size_t C_Port_capacity(C_Port* port, size_t *cptr);
void   C_Port_flush(C_Port* port);
bool   C_Port_is_output(C_Port* port);
bool   C_Port_is_writable(C_Port* port);
void   C_Port_write_char(C_Port* port, wchar_t char);
void   C_Port_write_newline(C_Port* port);
void   C_Port_write_raw(C_Port* port, 
                                    size_t bsz, 
                                    const octet_t* buf, 
                                    size_t *szptr);
void   C_Port_write_utf8(C_Port* port, 
                                    size_t bsz, 
                                    const utf8_t* buf, 
                                    size_t *szptr);

Closing Ports

/* closes and cleans up a Port */
void C_Port_close(C_Port* port);
bool C_Port_is_closed(C_Port* port);

Miscellaneous

C_Port_Env C_Port_env(C_Port* port);

Macros

/* Maybe it would be easier to throw real exceptions ... *sigh*. */

#define CPORT_TRY                     \
    { /* start try macro */           \
    C_Port* __cleanup = NULL;      \
    C_Port_Status __error = CPORT_OK;  \

#define CPORT_TRY_WITH(port)          \
    { /* start try macro */           \
    C_Port* __cleanup = (port);    \
    C_Port_Status __error = C_Port_status(__cleanup);    \
    if (__error != CPORT_OK) { goto catch; }

#define CPORT_CHECK(port)  \
    C_Port_Status __error = C_Port_status((port));    \
    if (__error != CPORT_OK) { goto catch; }

#define CPORT_CATCH(e)              catch: C_Port_Status (e) = __error;

#define CPORT_NO_CATCH              catch: goto endtry;

#define CPORT_END_TRY    \
    endtry: { if (__cleanup != NULL) C_Port_close(__cleanup); } \
    /* end try macro */ }

Standard Callbacks

File I/O (Unix)

#include <stdio.h>
name
Either a relative file name in the platform’s conventions or a File URL.
Open_File: Port Open
Open a file for reading (GET), writing (PUT), appending (POST), or reading and appending (CONNECT). Does nothing and returns NULL on other methods. Uses character mode on platforms where that matters. Returns the file handle if successful.
Read_File: Reader
Read bytes from the file if GET mode.
Write_File: Writer
Writes bytes to the file if PUT or POST mode.
Delete_File: Deleter
Deletes the named file.
Close_File: Port Close
Closes the file handle.

Socket I/O (Unix)

#include <sys/types.h>
#include <sys/socket.h>
name
Either “host:port” pair or a URL with a host and port.
Open_Socket: Port Open
Opens a client socket based on the hostname and port given in name. Uses full duplex if CONNECT, read-only if GET, write-only if PUT or POST. Does nothing and returns nothing on other methods. Returns the socket descriptor if successful.
Read_Socket: Reader
Read bytes from the socket.
Write_Socket: Writer
Write bytes to the socket.
Close_Socket: Port Close
Close both sides of the socket.

UNIX FIFO Pipe I/O

#include <sys/types.h>
#include <sys/stat.h>
#indlude <fcntl.h>
name
The file name or other identifier for a named pipe.
Open_Pipe: Port Open
Opens a named pipe based on name. Makes the pipe if it doesn’t exist. Opens for reading if GET, writing if PUT or POST, both if CONNECT. Does nothing and returns nothing on other methods. Returns the socket descriptor if successful.
Read_Pipe: Reader
Read from the UNIX pipe.
Write_Pipe: Writer
Write to the UNIX pipe.
Close_Pipe: Port Close
Close the UNIX pipe.

HTTP I/O

#include <sys/types.h>
#include <sys/socket.h>

(or maybe an HTTP client library …)

name
A full HTTP URL.
Open_Http_Get: Port Open (GET)
Sends an HTTP GET request with minimum required headers. Returns the socket descriptor if successful.
Open_Http_Post: Port Open (POST)
Prepares an HTTP POST request with minimum required headers. Returns the socket descriptor if successful.
Open_Http_Put: Port Open (PUT)
Prepares an HTTP PUT request with minimum required headers. Returns the socket descriptor if successful.
Open_Web_Socket: Port Open (CONNECT)
Sends handshake for a Web Socket. Returns the socket descriptor if successful.
Read_Http_Get: Reader (GET)
Read the HTTP GET response.
Read_Web_Socket: Reader (CONNECT)
Reads from a Web Socket.
Write_Http_Post: Writer (POST)
Write the POST body to a buffer.
Write_Http_Put: Write (PUT)
Write the PUT body to a buffer.
Write_Web_Socket: Writer (CONNECT)
Writes to a Web Socket.
Close_Http_Read: Port Close (GET, CONNECT)
Flush and close the socket.
Close_Http_Write: Port Close (PUT, POST)
Add the “Content-Length:” header, send the PUT or POST, parse the repsonse to get an appropriate C_Port_Status code to return.

  1. I’m assuming we can add macros to make the code look more like try-with-resource/catch/finally clauses in Java 8+. ↩︎

  2. Inspired / copied from the Lua interpreter’s low-level I/O. ↩︎