- Source
- foundation
- Chapter 2: Memory, Streams and Collections
- Writers and Loggers
Formatted text output to streams.
§1. Registration. The main function here is modelled on the "minimum printf
" function
used as an example in Kernighan and Ritchie, Chapter 7, but because it
prints to streams, it combines the traditional functions printf
, sprintf
and fprintf
in one. It also contains a number of doohickeys to provide
for a wider and extensible range of string interpolations.
Traditionally, in the C library, everything in the formatting string is
literal except for %
escapes: thus %d
means "integer goes here", and
so on. We follow this but allow extra %
escapes unknown to K&R, and we
also allow a further family of $
escapes intended for the debugging log
only; these are restricted to streams flagged as for debugging and generally
produce guru meditation numbers rather than user-friendly information.
Each escape, say %z
, must be "registered" before use, and will be
given one of the following categories:
define VACANT_ECAT 0 unregistered
define POINTER_ECAT 1 data to be printed is a pointer to a structure
define INTSIZED_ECAT 2 data to be printed is or fits into an integer
define WORDING_ECAT 3 data to be printed is a wording
structure from inform7
define DIRECT_ECAT 4 data must be printed directly by the code below
§2. We'll start with %
escapes, which generalise the familiar printf
escapes such as %d
. Cumbersomely, we need three sorts of escape: those where
the variable argument token is a pointer, those where it's essentially an
integer, and those where it's a structure used only in the Inform 7 compiler
called a wording
. The standard C typechecker can't generalise across these,
so we have to do everything three times. (And then we have to do all that twice,
because the loggers don't use format strings.)
int escapes_registered = FALSE;
int escapes_category[2][128]; one of the *_ECAT
values above
void *the_escapes[2][128]; the function to call to implement this
typedef void (*writer_function)(text_stream *, char *, void *);
typedef void (*writer_function_I)(text_stream *, char *, int);
typedef void (*log_function)(text_stream *, void *);
typedef void (*log_function_I)(text_stream *, int);
#ifdef WORDS_MODULE
typedef void (*writer_function_W)(text_stream *, char *, wording);
typedef void (*log_function_W)(text_stream *, wording);
#endif
void Writers::log_escape_usage(void) { for (int cat = 0; cat < 2; cat++) { char *alphanum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; LOG("Vacant escapes: %s: ", (cat == 0)?"%":"$"); for (int i=0; alphanum[i]; i++) if (escapes_category[cat][(int) alphanum[i]] == VACANT_ECAT) LOG("%c", alphanum[i]); else LOG("."); LOG("\n"); } }
The function Writers::log_escape_usage appears nowhere else.
§4. That gives us a number of front doors:
void Writers::register_writer(int esc, void (*f)(text_stream *, char *, void *)) { Writers::register_writer_p(0, esc, (void *) f, POINTER_ECAT); } void Writers::register_logger(int esc, void (*f)(text_stream *, void *)) { Writers::register_writer_p(1, esc, (void *) f, POINTER_ECAT); } void Writers::register_writer_I(int esc, void (*f)(text_stream *, char *, int)) { Writers::register_writer_p(0, esc, (void *) f, INTSIZED_ECAT); } void Writers::register_logger_I(int esc, void (*f)(text_stream *, int)) { Writers::register_writer_p(1, esc, (void *) f, INTSIZED_ECAT); } #ifdef WORDS_MODULE #define Writers::register_writer_W(esc, f) Writers::register_writer_p(0, esc, (void *) f, WORDING_ECAT); #define Writers::register_logger_W(esc, f) Writers::register_writer_p(1, esc, (void *) f, WORDING_ECAT); #endif
The function Writers::register_writer is used in 1/fnd (§8.1).
The function Writers::register_logger is used in 1/fnd (§8.3).
The function Writers::register_writer_I appears nowhere else.
The function Writers::register_logger_I appears nowhere else.
void Writers::register_writer_p(int set, int esc, void *f, int cat) { if (escapes_registered == FALSE) <Initialise the table of escapes 5.1>; if ((esc < 0) || (esc >= 128) || ((Characters::isalpha(esc) == FALSE) && (Characters::isdigit(esc) == FALSE))) internal_error("nonalphabetic escape"); if (escapes_category[set][esc] != VACANT_ECAT) { WRITE_TO(STDERR, "Clashing escape is %s%c\n", (set == 0)?"%":"$", esc); internal_error("clash of escapes"); } escapes_category[set][esc] = cat; the_escapes[set][esc] = f; }
The function Writers::register_writer_p is used in §4.
§5.1. We're going to implement %d
and a few others directly, so those are marked
in the table as being unavailable for registration.
Note that we don't support %f
for floats; but we do add our very own %w
for wide strings.
<Initialise the table of escapes 5.1> =
escapes_registered = TRUE; for (int e=0; e<2; e++) for (int i=0; i<128; i++) { the_escapes[e][i] = NULL; escapes_category[e][i] = VACANT_ECAT; } escapes_category[0]['c'] = DIRECT_ECAT; escapes_category[0]['d'] = DIRECT_ECAT; escapes_category[0]['g'] = DIRECT_ECAT; escapes_category[0]['i'] = DIRECT_ECAT; escapes_category[0]['s'] = DIRECT_ECAT; escapes_category[0]['w'] = DIRECT_ECAT; escapes_category[0]['x'] = DIRECT_ECAT; escapes_category[0]['%'] = DIRECT_ECAT; escapes_category[0]['$'] = DIRECT_ECAT; escapes_category[1]['%'] = DIRECT_ECAT; escapes_category[1]['$'] = DIRECT_ECAT;
This code is used in §5.
§6. Writing. We can finally get on with that formatted-print function we've all been waiting for:
void Writers::printf(text_stream *stream, char *fmt, ...) { va_list ap; the variable argument list signified by the dots char *p; if (stream == NULL) return; va_start(ap, fmt); macro to begin variable argument processing for (p = fmt; *p; p++) { switch (*p) { case '%': { int set = 0; <Deal with escape sequences 6.1>; break; } case '$': { int set = 1; if ((stream->stream_flags) & USES_LOG_ESCAPES_STRF) <Deal with escape sequences 6.1> else Streams::putc('$', stream); break; } case '"': if (stream->stream_flags & USES_I6_ESCAPES_STRF) Streams::putc('~', stream); else Streams::putc(*p, stream); break; case '\n': Streams::putc(*p, stream); break; default: Streams::putc(*p, stream); break; } } va_end(ap); macro to end variable argument processing }
The function Writers::printf is used in 2/str (§3).
§6.1.
<Deal with escape sequences 6.1> =
char format_string[8]; int esc_number = ' '; int i = 0; format_string[i++] = *(p++); while (*p) { format_string[i++] = *p; if ((islower(*p)) || (isupper(*p)) || ((set == 1) && (isdigit(*p))) || (*p == '%')) esc_number = (int) *p; p++; if ((esc_number != ' ') || (i==6)) break; } format_string[i] = 0; p--; if ((esc_number<0) || (esc_number > 255)) esc_number = 0; switch (escapes_category[set][esc_number]) { case POINTER_ECAT: { if (set == 0) { writer_function f = (writer_function) the_escapes[0][esc_number]; void *q = va_arg(ap, void *); (*f)(stream, format_string+1, q); } else { log_function f = (log_function) the_escapes[1][esc_number]; void *q = va_arg(ap, void *); (*f)(stream, q); } break; } case INTSIZED_ECAT: { if (set == 0) { writer_function_I f = (writer_function_I) the_escapes[0][esc_number]; int N = va_arg(ap, int); (*f)(stream, format_string+1, N); } else { log_function_I f = (log_function_I) the_escapes[1][esc_number]; int N = va_arg(ap, int); (*f)(stream, N); } break; } case WORDING_ECAT: { #ifdef WORDS_MODULE if (set == 0) { writer_function_W f = (writer_function_W) the_escapes[0][esc_number]; wording W = va_arg(ap, wording); (*f)(stream, format_string+1, W); } else { log_function_W f = (log_function_W) the_escapes[1][esc_number]; wording W = va_arg(ap, wording); (*f)(stream, W); } #endif break; } case DIRECT_ECAT: <Implement this using the original printf 6.1.1>; break; case VACANT_ECAT: WRITE_TO(STDERR, "*** Bad WRITE escape: <%s> ***\n", format_string); internal_error("Unknown string escape"); break; }
This code is used in §6 (twice).
§6.1.1. Here the traditional C library helps us out with the difficult ones to get
right. We don't trouble to check that correct printf
escapes have been used:
instead, we pass anything in the form of a percentage sign, followed by
up to four nonalphabetic modifying characters, followed by an alphabetic
category character for numerical printing, straight through to sprintf
or fprintf
.
Thus an escape like %04d
is handled by the standard C library, but not
%s
, which we handle directly. That's for two reasons: first, we want to
be careful to prevent overruns of memory streams; second, we need to ensure
that the correct encoding is used when writing to disc. The numerical
escapes involve only characters whose representation is the same in all our
file encodings, but expanding %s
does not.
<Implement this using the original printf 6.1.1> =
#pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-nonliteral" switch (esc_number) { case 'c': case 'd': case 'i': case 'x': {char
is promoted toint
in variable arguments int ival = va_arg(ap, int); char temp[256]; if (snprintf(temp, 255, format_string, ival) >= 255) strcpy(temp, "?"); for (int j = 0; temp[j]; j++) Streams::putc(temp[j], stream); break; } case 'g': { double dval = va_arg(ap, double); char temp[256]; if (snprintf(temp, 255, format_string, dval) >= 255) strcpy(temp, "?"); for (int j = 0; temp[j]; j++) Streams::putc(temp[j], stream); break; } case 's': for (char *sval = va_arg(ap, char *); *sval; sval++) Streams::putc(*sval, stream); break; case 'w': { wchar_t *W = (wchar_t *) va_arg(ap, wchar_t *); for (int j = 0; W[j]; j++) Streams::putc(W[j], stream); break; } case '%': Streams::putc('%', stream); break; case '$': Streams::putc('$', stream); break; } #pragma clang diagnostic pop
This code is used in §6.1.