[Writers::] Writers and Loggers. Formatted text output to streams. @h 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: @d VACANT_ECAT 0 /* unregistered */ @d POINTER_ECAT 1 /* data to be printed is a pointer to a structure */ @d INTSIZED_ECAT 2 /* data to be printed is or fits into an integer */ @d WORDING_ECAT 3 /* data to be printed is a |wording| structure from inform7 */ @d DIRECT_ECAT 4 /* data must be printed directly by the code below */ @ 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"); } } @ 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 @ All leading to: = void Writers::register_writer_p(int set, int esc, void *f, int cat) { if (escapes_registered == FALSE) @; if ((esc < 0) || (esc >= 128) || ((Characters::isalpha((wchar_t) esc) == FALSE) && (Characters::isdigit((wchar_t) 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; } @ 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. @ = 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; @h 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; @; break; } case '$': { int set = 1; if ((stream->stream_flags) & USES_LOG_ESCAPES_STRF) @ 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 */ } @ = 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: @; break; case VACANT_ECAT: WRITE_TO(STDERR, "*** Bad WRITE escape: <%s> ***\n", format_string); internal_error("Unknown string escape"); break; } @ 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. @ = #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wformat-nonliteral" switch (esc_number) { case 'c': case 'd': case 'i': case 'x': { /* |char| is promoted to |int| in variable arguments */ int ival = va_arg(ap, int); char temp[256]; if (PLATFORM_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 (PLATFORM_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 @h Abbreviation macros. The following proved convenient for Inform, at any rate. @d REGISTER_WRITER(c, f) Writers::register_logger(c, &f##_writer); @d COMPILE_WRITER(t, f) void f##_writer(text_stream *format, void *obj) { text_stream *SDL = DL; DL = format; if (DL) f((t) obj); DL = SDL; } @d REGISTER_WRITER_I(c, f) Writers::register_logger_I(c, &f##_writer); @d COMPILE_WRITER_I(t, f) void f##_writer(text_stream *format, int I) { text_stream *SDL = DL; DL = format; if (DL) f((t) I); DL = SDL; }