diff --git a/RELEASES.rst b/RELEASES.rst index ec72fd26..66c8eac4 100644 --- a/RELEASES.rst +++ b/RELEASES.rst @@ -1665,6 +1665,12 @@ Planned * Add an extra module (extras/console) providing a minimal 'console' binding (GH-767) +* Add an extra module (extras/minimal-printf) providing minimal, + Duktape-optimized sprintf(), snprintf(), vsnprintf(), and sscanf() + implementations; the extra compiles to less than 1kB of code which is + useful on bare metal platforms where an external printf() or scanf() + dependency may have a large footprint impact (often 10-30 kB) (GH-801) + * Fix a harmless compilation warning related to a shadowed variable (GH-793, GH-794) diff --git a/config/other-defines/platform_functions.yaml b/config/other-defines/platform_functions.yaml index 23857549..19aff648 100644 --- a/config/other-defines/platform_functions.yaml +++ b/config/other-defines/platform_functions.yaml @@ -43,7 +43,7 @@ # String printing and parsing - define: DUK_SPRINTF - define: DUK_SNPRINTF -- define: DUK_VSNPRINT +- define: DUK_VSNPRINTF - define: DUK_SSCANF - define: DUK_VSSCANF diff --git a/extras/minimal-printf/Makefile b/extras/minimal-printf/Makefile new file mode 100644 index 00000000..aa26c0d3 --- /dev/null +++ b/extras/minimal-printf/Makefile @@ -0,0 +1,5 @@ +# Just for manual testing +.PHONY: test +test: duk_minimal_printf.c + gcc -fno-stack-protector -m32 -otest -Wall -Wextra -Os -fomit-frame-pointer duk_minimal_printf.c test.c + ./test diff --git a/extras/minimal-printf/README.rst b/extras/minimal-printf/README.rst new file mode 100644 index 00000000..e13076c1 --- /dev/null +++ b/extras/minimal-printf/README.rst @@ -0,0 +1,110 @@ +============================================== +Minimal sprintf/sscanf replacement for Duktape +============================================== + +The ``duk_minimal_printf.c`` provides a portable provider for sprintf()/scanf() +with a feature set matching minimally what Duktape needs. The provider +compiles to less than 1kB. The functions provided are:: + + sprintf() + snprintf() + vsnprintf() + sscanf() + +Assumptions: + +* ``sizeof(void *) <= sizeof(long)`` + +* ``sizeof(long) <= 8`` + +Note that these assumptions don't hold e.g. on 64-bit Windows. This printf +provider is mostly useful for low memory targets where these assumptions are +typically not an issue. The limitations are easy to fix if one relies more +on platform typing. + +Supported formatting +==================== + +sprintf() +--------- + +Duktape relies on a ``sprintf()`` provider which supports at least the +following (this list is from Duktape 1.5.0):: + + %c + %s + %p + + %02d + %03d + %ld + %lld (JSON fast path only) + + %lu + + %lx + %02lx + %08lx + +This minimal provider supports the following slightly different set: + +* Character format ``%c``. + +* String format ``%s``. + +* Pointer format ``%p``. + +* Integer formats ``%d``, ``%ld``, ``%lu`` with optional padding and + length modifiers. + +* Hex formats ``%x``, ``%lx`` with optional padding and length modifiers. + +The ``%lld`` format is not supported to avoid depending on the ``long long`` +type; this makes the replacement incompatible with the JSON fast path which +must thus be disabled. + +sscanf() +-------- + +There's only one call site for ``sscanf()``, for JX parsing of pointers:: + + duk_bi_json.c: (void) DUK_SSCANF((const char *) js_ctx->p, DUK_STR_FMT_PTR, &voidptr); + +The exact format string here is ``%p`` and nothing else needs to be supported. +Further, when the minimal printf/scanf providers are used together we only +need to parse what we produce. In particular: + +* Pointer prefix is ``0x``, no need to match ``0X`` for example. + +* All digits are ``[0-9a-f]`` with no need to match uppercase. + +Building "duk" with minimal printf/scanf +======================================== + +The necessary defines in ``duk_config.h`` can be given to genconfig, but you +can also just make the following manual additions to the bottom of the config +file:: + + #include "duk_minimal_printf.h" + + #undef DUK_SPRINTF + #define DUK_SPRINTF duk_minimal_sprintf + #undef DUK_SNPRINTF + #define DUK_SNPRINTF duk_minimal_snprintf + #undef DUK_VSNPRINTF + #define DUK_VSNPRINTF duk_minimal_vsnprintf + #undef DUK_SSCANF + #define DUK_SSCANF duk_minimal_sscanf + +Then just add ``duk_minimal_printf.c`` to build and compile the application. + +Future work +=========== + +* Add support for ``%lld`` (maybe conditional) to allow JSON fast path to + be supported. + +* Add support for platforms such as 64-bit Windows where + ``sizeof(long) < sizeof(void *)``. This can be achieved by using a few + typedefs internally; typedef an integer type large enough to hold all + formatted types. diff --git a/extras/minimal-printf/duk_minimal_printf.c b/extras/minimal-printf/duk_minimal_printf.c new file mode 100644 index 00000000..2d3bcf3a --- /dev/null +++ b/extras/minimal-printf/duk_minimal_printf.c @@ -0,0 +1,271 @@ +/* + * Minimal sprintf() for Duktape. + */ + +#include /* va_list etc */ +#include /* size_t */ +#include /* SIZE_MAX */ + +#define DUK__WRITE_CHAR(c) do { \ + if (off < size) { \ + str[off] = (char) c; \ + } \ + off++; \ + } while (0) + +static const char duk__fmt_nybbles[16] = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' +}; + +static size_t duk__format_long(char *str, size_t size, size_t off, int fixed_length, char pad, int radix, int neg_sign, unsigned long v) { + char buf[24]; /* 2^64 = 18446744073709552000, length 20 */ + char *required; + char *p; + int i; + + /* Format in reverse order first. Ensure at least one digit is output + * to handle '0' correctly. Note that space padding and zero padding + * handle negative sign differently: + * + * %9d and -321 => ' -321' + * %09d and -321 => '-00000321' + */ + + for (i = 0; i < (int) sizeof(buf); i++) { + buf[i] = pad; /* compiles into memset() equivalent, avoid memset() dependency */ + } + + p = buf; + do { + *p++ = duk__fmt_nybbles[v % radix]; + v /= radix; + } while (v != 0); + + required = buf + fixed_length; + if (pad == (char) '0' && fixed_length > 0 /* handle "%0d" correctly, though insane */) { + /* Leave space for negative sign. */ + if (p < required - neg_sign) { + p = required - neg_sign; + } + } + if (neg_sign) { + *p++ = '-'; + } + if (p < required) { + p = required; + } + + /* Now [buf,p[ contains the result in reverse; copy into place. */ + + while (p > buf) { + p--; + DUK__WRITE_CHAR(*p); + } + + return off; +} + +static int duk__parse_pointer(const char *str, void **out) { + const unsigned char *p; + long val; /* assume void * fits into long */ + int count; + unsigned char ch; + + /* We only need to parse what our minimal printf() produces, so that + * we can check for a '0x' prefix, and assume all hex digits are + * lowercase. + */ + + p = (const unsigned char *) str; + if (*p++ != (unsigned char) '0') { + return 0; + } + if (*p++ != (unsigned char) 'x') { + return 0; + } + + for (val = 0, count = 0; count < (int) (sizeof(void *) * 2); count++) { + ch = *p++; + + val <<= 4; + if (ch >= (unsigned char) '0' && ch <= (unsigned char) '9') { + val += ch - (unsigned char) '0'; + } else if (ch >= (unsigned char) 'a' && ch <= (unsigned char) 'f') { + val += ch - (unsigned char) 'a' + 0x0a; + } else { + return 0; + } + } + + /* The input may end at a NUL or garbage may follow. As long as we + * parse the '%p' correctly, garbage is allowed to follow, and the + * JX pointer parsing also relies on that. + */ + + *out = (void *) val; + return 1; +} + +int duk_minimal_vsnprintf(char *str, size_t size, const char *format, va_list ap) { + size_t off = 0; + const char *p; + const char *p_tmp; + const char *p_fmt_start; + char c; + char pad; + int fixed_length; + int is_long; + + /* Assume str != NULL unless size == 0. + * Assume format != NULL. + */ + + p = format; + for (;;) { + c = *p++; + if (c == (char) 0) { + break; + } + if (c != (char) '%') { + DUK__WRITE_CHAR(c); + continue; + } + + /* Start format sequence. Scan flags and format specifier. */ + + p_fmt_start = p - 1; + is_long = 0; + pad = ' '; + fixed_length = 0; + for (;;) { + c = *p++; + if (c == (char) 'l') { + is_long = 1; + } else if (c == (char) '0') { + /* Only support pad character '0'. */ + pad = '0'; + } else if (c >= (char) '1' && c <= (char) '9') { + /* Only support fixed lengths 1-9. */ + fixed_length = (int) (c - (char) '0'); + } else if (c == (char) 'd') { + long v; + int neg_sign = 0; + if (is_long) { + v = va_arg(ap, long); + } else { + v = (long) va_arg(ap, int); + } + if (v < 0) { + neg_sign = 1; + v = -v; + } + off = duk__format_long(str, size, off, fixed_length, pad, 10, neg_sign, (unsigned long) v); + break; + } else if (c == (char) 'u') { + unsigned long v; + if (is_long) { + v = va_arg(ap, unsigned long); + } else { + v = (unsigned long) va_arg(ap, unsigned int); + } + off = duk__format_long(str, size, off, fixed_length, pad, 10, 0, v); + break; + } else if (c == (char) 'x') { + unsigned long v; + if (is_long) { + v = va_arg(ap, unsigned long); + } else { + v = (unsigned long) va_arg(ap, unsigned int); + } + off = duk__format_long(str, size, off, fixed_length, pad, 16, 0, v); + break; + } else if (c == (char) 'c') { + char v; + v = va_arg(ap, int); /* intentionally not 'char' */ + DUK__WRITE_CHAR(v); + break; + } else if (c == (char) 's') { + const char *v; + char c_tmp; + v = va_arg(ap, const char *); + if (v) { + for (;;) { + c_tmp = *v++; + if (c_tmp) { + DUK__WRITE_CHAR(c_tmp); + } else { + break; + } + } + } + break; + } else if (c == (char) 'p') { + /* Assume a void * can be represented by 'long'. This is not + * always the case. NULL pointer is printed out as 0x0000... + */ + void *v; + v = va_arg(ap, void *); + DUK__WRITE_CHAR('0'); + DUK__WRITE_CHAR('x'); + off = duk__format_long(str, size, off, sizeof(void *) * 2, '0', 16, 0, (unsigned long) v); + break; + } else { + /* Unrecognized, just copy verbatim. */ +#if 0 + DUK__WRITE_CHAR('!'); +#endif + for (p_tmp = p_fmt_start; p_tmp != p; p_tmp++) { + DUK__WRITE_CHAR(*p_tmp); + } + break; + } + } + } + + if (off < size) { + str[off] = (char) 0; /* No increment for 'off', not counted in return value. */ + } else if (size > 0) { + /* Forced termination. */ + str[size - 1] = 0; + } + + return (int) off; +} + +int duk_minimal_snprintf(char *str, size_t size, const char *format, ...) { + va_list ap; + int ret; + va_start(ap, format); + ret = duk_minimal_vsnprintf(str, size, format, ap); + va_end(ap); + return ret; +} + +int duk_minimal_sprintf(char *str, const char *format, ...) { + va_list ap; + int ret; + va_start(ap, format); + ret = duk_minimal_vsnprintf(str, SIZE_MAX, format, ap); + va_end(ap); + return ret; +} + +int duk_minimal_sscanf(const char *str, const char *format, ...) { + va_list ap; + int ret; + void **out; + + /* Only the exact "%p" format is supported. */ + if (format[0] != (char) '%' || format[1] != (char) 'p' || + format[2] != (char) 0) { + } + + va_start(ap, format); + out = va_arg(ap, void **); + ret = duk__parse_pointer(str, out); + va_end(ap); + + return ret; +} + +#undef DUK__WRITE_CHAR diff --git a/extras/minimal-printf/duk_minimal_printf.h b/extras/minimal-printf/duk_minimal_printf.h new file mode 100644 index 00000000..f0aa6b58 --- /dev/null +++ b/extras/minimal-printf/duk_minimal_printf.h @@ -0,0 +1,12 @@ +#if !defined(DUK_MINIMAL_PRINTF_H_INCLUDED) +#define DUK_MINIMAL_PRINTF_H_INCLUDED + +#include /* va_list etc */ +#include /* size_t */ + +extern int duk_minimal_sprintf(char *str, const char *format, ...); +extern int duk_minimal_snprintf(char *str, size_t size, const char *format, ...); +extern int duk_minimal_vsnprintf(char *str, size_t size, const char *format, va_list ap); +extern int duk_minimal_sscanf(const char *str, const char *format, ...); + +#endif /* DUK_MINIMAL_PRINTF_H_INCLUDED */ diff --git a/extras/minimal-printf/test.c b/extras/minimal-printf/test.c new file mode 100644 index 00000000..19a407aa --- /dev/null +++ b/extras/minimal-printf/test.c @@ -0,0 +1,190 @@ +#include +#include +#include "duk_minimal_printf.h" + +char buffer[32]; + +static void init_buffer(void) { + int i; + + for (i = 0; i < (int) sizeof(buffer); i++) { + buffer[i] = 0xff; + } +} + +static void dump_buffer(void) { + int i; + unsigned char c; + + printf("Buffer: '"); + for (i = 0; i < (int) sizeof(buffer); i++) { + c = (unsigned char) buffer[i]; + if (c < 0x20 || c >= 0x7e) { + printf("<%02x>", (unsigned int) c); + } else { + printf("%c", (int) c); + } + } + printf("'"); +#if 0 + printf(" -> "); + printf("Buffer:"); + for (i = 0; i < sizeof(buffer); i++) { + c = (unsigned char) buffer[i]; + if (c <= 0x20 || c >= 0x7e) { + printf(" <%02x>", (unsigned int) c); + } else { + printf(" %c", (char) c); + } + } +#endif + printf("\n"); +} + +int main(int argc, char *argv[]) { + int ret; + void *voidptr; + int i; + + (void) argc; (void) argv; + + /* Char format. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "foo %c bar", 'Z'); + dump_buffer(); + + /* Signed long format. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%ld %9ld", (long) 123, (long) 4321); + dump_buffer(); + + /* Signed long with zero padding. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%09ld", (long) 4321); + dump_buffer(); + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%03ld %03ld %03ld", (long) -4321, (long) -432, (long) -43); + dump_buffer(); + + /* Unsigned long with zero padding. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%03lu %03lu %03lu", (long) -4321, (long) -432, (long) -43); + dump_buffer(); + + /* Signed integer. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%d %9d", (int) 0, (int) 4321); + dump_buffer(); + + /* Signed negative integer, fixed field width. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%9d", (int) -321); + dump_buffer(); + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%09d", (int) -321); + dump_buffer(); + printf(" -- printf comparison: %9d %09d\n", -321, -321); + + /* Hex formatting. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%03x %03lx 0x%08lx", (int) 510, (long) 5105, (long) 0xdeadbeef); + dump_buffer(); + + /* Pointer formatting, NULL and non-NULL. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%p %p", (void *) NULL, (void *) buffer); + dump_buffer(); + + /* File/line like format test. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%s:%d", "foo bar quux", 123); + dump_buffer(); + + /* Zero size output buffer. */ + init_buffer(); + duk_minimal_snprintf(buffer, 0, "%s:%d", "foo bar quux", 123); + dump_buffer(); + init_buffer(); + duk_minimal_snprintf(buffer, 0, ""); + dump_buffer(); + + /* NUL terminator boundary test. */ + init_buffer(); + duk_minimal_snprintf(buffer, 7, "foo: %s", "bar"); + dump_buffer(); + init_buffer(); + duk_minimal_snprintf(buffer, 8, "foo: %s", "bar"); + dump_buffer(); + init_buffer(); + duk_minimal_snprintf(buffer, 9, "foo: %s", "bar"); + dump_buffer(); + + /* sprintf() binding, uses SIZE_MAX internally. */ + init_buffer(); + duk_minimal_sprintf(buffer, "unbounded print %s", "foo"); + dump_buffer(); + + /* Pointer formatting; non-NULL and NULL. */ + init_buffer(); + duk_minimal_snprintf(buffer, sizeof(buffer), "%p %p", (void *) NULL, (void *) 0xdeadbeef); + dump_buffer(); + + /* Pointer parsing, non-NULL (32-bit) pointer. */ + voidptr = (void *) 123; + ret = duk_minimal_sscanf("0xdeadbeef", "%p", &voidptr); + printf("ret=%d, void pointer: %p\n", ret, voidptr); + + /* Pointer parsing, NULL (32-bit) pointer. */ + voidptr = (void *) 123; + ret = duk_minimal_sscanf("0x00000000", "%p", &voidptr); + printf("ret=%d, void pointer: %p\n", ret, voidptr); + + /* Pointer parsing, non-NULL (32-bit) pointer but garbage follows. */ + voidptr = (void *) 123; + ret = duk_minimal_sscanf("0xdeadbeefx", "%p", &voidptr); + printf("ret=%d, void pointer: %p\n", ret, voidptr); + + /* Fixed width test over a range of widths. */ + for (i = 0; i <= 9; i++) { + char fmtbuf[16]; + + printf("--- pos/neg fixed width test, i=%d\n", i); + + /* %0d. %00d makes no sense, but tested anyway. */ + memset((void *) fmtbuf, 0, sizeof(fmtbuf)); + fmtbuf[0] = (char) '%'; + fmtbuf[1] = (char) '0'; + fmtbuf[2] = (char) ('0' + i); + fmtbuf[3] = 'd'; + init_buffer(); + duk_minimal_sprintf(buffer, (const char *) fmtbuf, 321); + dump_buffer(); + init_buffer(); + duk_minimal_sprintf(buffer, (const char *) fmtbuf, -321); + dump_buffer(); + printf(" ==> printf: |"); + printf((const char *) fmtbuf, 321); + printf("| |"); + printf((const char *) fmtbuf, -321); + printf("|\n"); + + /* %d. */ + memset((void *) fmtbuf, 0, sizeof(fmtbuf)); + fmtbuf[0] = (char) '%'; + fmtbuf[1] = (char) ('0' + i); + fmtbuf[2] = 'd'; + init_buffer(); + duk_minimal_sprintf(buffer, (const char *) fmtbuf, 321); + dump_buffer(); + init_buffer(); + duk_minimal_sprintf(buffer, (const char *) fmtbuf, -321); + dump_buffer(); + printf(" ==> printf: |"); + printf((const char *) fmtbuf, 321); + printf("| |"); + printf((const char *) fmtbuf, -321); + printf("|\n"); + } + + return 0; +} diff --git a/util/make_dist.py b/util/make_dist.py index 3288ad4e..19d8e483 100644 --- a/util/make_dist.py +++ b/util/make_dist.py @@ -158,6 +158,7 @@ def create_dist_directories(dist): mkdir(os.path.join(dist, 'extras', 'print-alert')) mkdir(os.path.join(dist, 'extras', 'console')) mkdir(os.path.join(dist, 'extras', 'logging')) + mkdir(os.path.join(dist, 'extras', 'minimal-printf')) mkdir(os.path.join(dist, 'polyfills')) #mkdir(os.path.join(dist, 'doc')) # Empty, so omit mkdir(os.path.join(dist, 'licenses')) @@ -608,6 +609,14 @@ copy_files([ 'Makefile' ], os.path.join('extras', 'console'), os.path.join(dist, 'extras', 'console')) +copy_files([ + 'README.rst', + 'duk_minimal_printf.c', + 'duk_minimal_printf.h', + 'Makefile', + 'test.c' +], os.path.join('extras', 'minimal-printf'), os.path.join(dist, 'extras', 'minimal-printf')) + copy_files([ 'Makefile.cmdline', 'Makefile.dukdebug',