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:
-
The set of resources (file system, network, etc.) the ports may access.
-
The types of protocols – “file”, “http”, “https”, “ftp”, etc. – those resources support.
-
The set of operations – read (GET), append (POST), overwrite (PUT), two-way read/write (CONNECT), and remove (DELETE) – a port may perform on a resource.
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:
-
env is an Env structure created by
C_Port_open()
. -
method is an enum for HTTP-like methods.
-
path can be either a URI or platform specific path.
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.