Description¶
Exess is a simple C library for reading and writing values as strings.
It provides portable locale-independent functions for converting common number,
date, time, and binary datatypes. Values are preserved exactly wherever
possible, so, for example, a float written to a string will be read back as
exactly the original value on any system.
The supported datatypes, defined by the XSD specification, are explicitly compatible with many standards (like XML, RDF, and ISO 8601), and most are incidentally compatible with many others (like C and JSON).
The library is straightforward to use and doesn’t depend on an allocator, any other blocking functions, the current locale, or any other shared mutable state, making it safe to use in almost any context.
Supported Datatypes¶
Most of the XSD datatypes are implemented, omitting some XML-specific and recurring Gregorian calendar datatypes:
boolean, like “false”, “true”, “0”, or “1”.decimal, like “1.234”.floatanddouble, like “4.2E1” or “4.2e1”.The unbounded integers
integer,nonPositiveInteger,negativeInteger,nonNegativeInteger, andnonPositiveInteger, like “56” or “-78”.The constrained integers
long,int,short,byte,unsignedLong,unsignedInt,unsignedShort, andunsignedByte, like “-90” or “12”.duration, like “P1Y2M3DT4H5M”, and its totally-ordered derivativesyearMonthDuration(like “P6Y7M”) anddayTimeDuration(like “P8DT9H”).dateTime, like “2001-01-30T14:30:45”, and its totally-ordered derivativedateTimeStamp(like “1929-10-24T09:30:00-04:00”).time, like “12:30:00.000000001”.date, like “2001-12-31”.hexBinary, like “EC5355”.base64Binary, like “Zm9vYmFy”.
Precision¶
Time-related datatypes have nanosecond precision.
Precision of decimals and unbounded integers is limited only by the provided buffer size. These arbitrary-precision values are opaque, but can be coerced to simpler datatypes for use.
Dependencies¶
Exess has no dependencies except for the C standard library, specifically the
functions ceil, ldexp, llrint, llrintf, memcmp, memcpy, memset,
nextafter, strcmp, strncmp, trunc, and truncf.
Usage¶
To use exess,
the compiler must be configured to add the versioned include directory to the include path,
and to link with the corresponding library.
The pkg-config package exess-0 describes the required options:
pkg-config --cflags --libs exess-0
The API can then be used by including exess/exess.h:
#include <exess/exess.h>
If package support isn’t available,
arguments like -I/usr/include/exess-0 -lexess-0 must be added to the compiler command manually.
Reading Values¶
Each supported type has a read function that takes a string to read, and a pointer to an output value.
It reads the value after skipping any leading whitespace,
then returns an ExessResult with a status code and the count of characters read.
For example:
int32_t v = 0;
ExessResult r = exess_read_int("1234", &v);
if (!r.status) {
printf("Read %zu bytes as %d\n", r.count, v);
}
If there was a syntax error,
the status gives the specific problem,
and the count gives the offset of the error.
Note that read functions may stop at unrecognized non-whitespace characters and return successfully. This allows values to be read directly from any syntactic context (for example, a quoted field in a document string), but requires the caller to check that the read ended where expected. Proper error detection requires checking the returned count as well, for example:
bool is_null_terminated_int(const char* string) {
int32_t v = 0;
ExessResult r = exess_read_int(string, &v);
return !r.status && !string[r.count];
}
Writing Values¶
The corresponding write function takes a value to write,
a buffer size in bytes, and a buffer to write to.
It returns an ExessResult,
with a status code and the count of characters written,
not including the trailing null byte.
char s[12] = {0};
ExessResult r = exess_write_int(1234, sizeof(s), s);
if (!r.status) {
printf("Write error: %s\n", exess_strerror(r.status));
}
Allocating Strings¶
Exess never allocates memory, the calling code is responsible for providing a large enough output buffer.
For datatypes with a bounded length,
the array exess_max_lengths,
indexed by the ExessDatatype,
contains the length of the longest possible string value of that datatype (at most 41),
or zero if the datatype is unbounded.
For larger values, the required length can be determined by passing a zero output size to the write function.
Then, EXESS_NO_SPACE is returned with the count of characters required.
This can be used to precisely allocate memory for the string,
taking care to allocate an extra byte for the null terminator.
For example:
ExessResult r = exess_write_int(1234, 0, NULL);
char* s = (char*)calloc(r.count + 1, 1);
r = exess_write_int(1234, r.count + 1, s);
Note that for some types, measuring the output can be about as expensive as actually writing the value. For example, it requires binary to decimal conversion for floating point numbers. So, it’s usually best to avoid measuring twice by writing immediately to a sufficiently large buffer, then copying the result to the final destination.
Generic Values¶
The fundamental read and write functions all have similar semantics but different type signatures. A generic API that works with opaque buffers is also provided, which can be used to read and write any supported datatype without explicitly handling each case.
Any value can be read with exess_read_value() and written with exess_write_value(),
which work similarly to their typed counterparts,
except they take a datatype, size, and pointer to a buffer rather than a value.
ExessDatatype enumerates all of the supported datatypes.
Unbounded Numbers¶
There are six unbounded number types:
decimal,
integer,
nonPositiveInteger,
negativeInteger,
nonNegativeInteger,
and positiveInteger.
Both exess_read_value() and exess_write_value() support reading and writing a subset of these types,
but “big” numbers aren’t supported.
Values are stored in the largest corresponding native type:
double, int64_t, or uint64_t.
If the value doesn’t fit,
then exess_read_value() will return an EXESS_OUT_OF_RANGE error.
Canonical Writing¶
Since values are usually written in canonical form,
strings can be converted to canonical form by first reading, then writing again.
If the parsed value itself isn’t required,
then exess_write_canonical() can be used to do this more efficiently.
For example, this will print 12:
char s[4] = {0};
ExessVariableResult r = {EXESS_SUCCESS, 0, 0};
r = exess_write_canonical(EXESS_INT, "+12", sizeof(s), s);
if (!r.status) {
printf("%s\n", s);
}
This is particularly useful for unbounded datatypes, since values are transformed one character at a time, avoiding value conversion, machine limits, and the need for a temporary value buffer.