Skip to content

cwrap with returnType "boolean" returns a number when all args are numeric #27021

@esgott

Description

@esgott

Summary

cwrap(ident, "boolean", argTypes) returns a JS number (0 or 1) instead of a boolean when argTypes consists only of "number" / "boolean". This is inconsistent with ccall(ident, "boolean", argTypes, args), which correctly returns a JS boolean. The cwrap docs list "boolean" as a valid return type, so the fast-path skip is a silent contract violation.

Cause

In src/library_ccall.js, cwrap's fast-path test treats "boolean" returns as numeric:

var cwrap = (ident, returnType, argTypes, opts) => {
  var numericArgs = !argTypes || argTypes.every((type) => type === "number" || type === "boolean");
  var numericRet = returnType !== "string";   // ← "boolean" counted as numeric
  if (numericRet && numericArgs && !opts) return getCFunc(ident);
  return (...args) => ccall(ident, returnType, argTypes, args, opts);
};

When the fast path triggers, getCFunc(ident) is the raw WASM export, so Boolean(ret) from ccall's convertReturnValue is never applied.

The asymmetry traces back to #17511, which extended the fast path to accept "boolean" as an argument type but didn't update the return-type check correspondingly.

Reproduction

repro.c:

#include <stdbool.h>

bool some_bool_func(int x) {
  return x != 0;
}

Build:

emcc repro.c -o repro.mjs \
  -sMODULARIZE=1 -sEXPORT_ES6=1 \
  -sENVIRONMENT=node \
  -sEXPORTED_FUNCTIONS='["_some_bool_func"]' \
  -sEXPORTED_RUNTIME_METHODS='["cwrap","ccall"]'

test.mjs:

import createModule from "./repro.mjs";

const mod = await createModule();
const viaCwrap = mod.cwrap("some_bool_func", "boolean", ["number"]);

const r = viaCwrap(1);
console.log("cwrap result:", r, "typeof:", typeof r);
console.log("ccall result:", mod.ccall("some_bool_func", "boolean", ["number"], [1]));

Run: node test.mjs

Actual:

cwrap result: 1 typeof: number
ccall result: true

Expected:

cwrap result: true typeof: boolean
ccall result: true

Impact

For plain-JS callers, if (result) truthiness coercion masks the bug. It surfaces for strictly-typed consumers (Scala.js, Kotlin/JS, TypeScript when the cwrap return is annotated boolean) — the value crosses the JS↔host boundary as a number and the host's type check rejects the cast.

Possible fixes

  1. Exclude "boolean" from the fast-path return check: var numericRet = returnType !== "string" && returnType !== "boolean"; — falls through to ccall, which already does Boolean(ret). Smallest diff, correctness wins.
  2. Keep the fast path but wrap with a Boolean() thunk when returnType === "boolean". Preserves the per-call perf win.
  3. Document "boolean" as unsupported as a cwrap return type (only valid via ccall).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions