diff --git a/quickjs.c b/quickjs.c
index 7916013..3936eec 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -67,6 +67,16 @@
 #define CONFIG_PRINTF_RNDN
 #endif
 
+#ifdef CONFIG_PRINTF_RNDN
+#if !defined(FE_DOWNWARD) || !defined(FE_UPWARD)
+#ifdef CONFIG_BIGNUM
+#define CONFIG_DTOA_LIBBF
+#else
+#error "CONFIG_BIGNUM required if printf is RNDN and there is no fenv support"
+#endif
+#endif
+#endif
+
 /* define to include Atomics.* operations which depend on the OS
    threads */
 #if !defined(EMSCRIPTEN)
@@ -11299,6 +11309,11 @@ static char *i64toa(char *buf_end, int64_t n, unsigned int base)
     return q;
 }
 
+/* maximum buffer size for js_dtoa */
+#define JS_DTOA_BUF_SIZE 128
+
+#ifndef CONFIG_DTOA_LIBBF
+
 /* buf1 contains the printf result */
 static void js_ecvt1(double d, int n_digits, int *decpt, int *sign, char *buf,
                      int rounding_mode, char *buf1, int buf1_size)
@@ -11318,9 +11333,6 @@ static void js_ecvt1(double d, int n_digits, int *decpt, int *sign, char *buf,
     *decpt = atoi(buf1 + n_digits + 2 + (n_digits > 1)) + 1;
 }
 
-/* maximum buffer size for js_dtoa */
-#define JS_DTOA_BUF_SIZE 128
-
 /* needed because ecvt usually limits the number of digits to
    17. Return the number of digits. */
 static int js_ecvt(double d, int n_digits, int *decpt, int *sign, char *buf,
@@ -11429,6 +11441,8 @@ static void js_fcvt(char *buf, int buf_size, double d, int n_digits)
     js_fcvt1(buf, buf_size, d, n_digits, rounding_mode);
 }
 
+#endif /* CONFIG_DTOA_LIBBF */
+
 /* radix != 10 is only supported with flags = JS_DTOA_VAR_FORMAT */
 /* use as many digits as necessary */
 #define JS_DTOA_VAR_FORMAT   (0 << 0)
@@ -11442,8 +11456,10 @@ static void js_fcvt(char *buf, int buf_size, double d, int n_digits)
 /* XXX: slow and maybe not fully correct. Use libbf when it is fast enough.
    XXX: radix != 10 is only supported for small integers
 */
-static void js_dtoa1(char *buf, double d, int radix, int n_digits, int flags)
+static JSValue js_dtoa(JSContext *ctx,
+                       double d, int radix, int n_digits, int flags)
 {
+    char buf[JS_DTOA_BUF_SIZE];
     char *q;
 
     if (!isfinite(d)) {
@@ -11465,6 +11481,25 @@ static void js_dtoa1(char *buf, double d, int radix, int n_digits, int flags)
         ptr = i64toa(buf1 + sizeof(buf1), i64, radix);
         strcpy(buf, ptr);
     } else {
+#ifdef CONFIG_DTOA_LIBBF
+        bf_flags_t bf_flags;
+    generic_conv:
+        bf_flags = BF_RNDNA;
+        switch (flags & 3) {
+        case JS_DTOA_VAR_FORMAT:
+            bf_flags |= BF_FTOA_FORMAT_FREE_MIN;
+            break;
+        case JS_DTOA_FIXED_FORMAT:
+            bf_flags |= BF_FTOA_FORMAT_FIXED;
+            break;
+        case JS_DTOA_FRAC_FORMAT:
+            bf_flags |= BF_FTOA_FORMAT_FRAC;
+            break;
+        }
+        if (flags & JS_DTOA_FORCE_EXP)
+            bf_flags |= BF_FTOA_FORCE_EXP;
+        return js_ftoa(ctx, JS_NewFloat64(ctx, d), radix, n_digits, bf_flags);
+#else /* CONFIG_DTOA_LIBBF */
         if (d == 0.0)
             d = 0.0; /* convert -0 to 0 */
         if (flags == JS_DTOA_FRAC_FORMAT) {
@@ -11528,14 +11563,8 @@ static void js_dtoa1(char *buf, double d, int radix, int n_digits, int flags)
                 sprintf(q, "%d", p);
             }
         }
+#endif /* CONFIG_DTOA_LIBBF */
     }
-}
-
-static JSValue js_dtoa(JSContext *ctx,
-                       double d, int radix, int n_digits, int flags)
-{
-    char buf[JS_DTOA_BUF_SIZE];
-    js_dtoa1(buf, d, radix, n_digits, flags);
     return JS_NewString(ctx, buf);
 }
 
